From 31f36aa9c28a794f8a3e93515f07330765915741 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 17 Jan 2018 08:48:07 +1100 Subject: [PATCH 001/163] reshaping droppable dimension for clarity --- src/state/dimension.js | 18 ++++++++++-------- src/state/get-droppable-over.js | 8 +++----- .../get-best-cross-axis-droppable.js | 13 +++++++++---- src/types.js | 3 ++- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/state/dimension.js b/src/state/dimension.js index ce70dc1854..b9a02c2274 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -121,12 +121,19 @@ export const scrollDroppable = ( newScroll: Position ): DroppableDimension => { const existing: DroppableDimensionViewport = droppable.viewport; + const frame: ?Area = existing.frame; + + if (frame == null) { + console.error('Cannot scroll Droppable that does not have a frame'); + return droppable; + } const scrollDiff: Position = subtract(newScroll, existing.frameScroll.initial); // a positive scroll difference leads to a negative displacement // (scrolling down pulls an item upwards) const scrollDisplacement: Position = negate(scrollDiff); const displacedSubject: Spacing = offset(existing.subject, scrollDisplacement); + const clipped: ?Area = clip(frame, displacedSubject); const viewport: DroppableDimensionViewport = { // does not change @@ -141,7 +148,7 @@ export const scrollDroppable = ( displacement: scrollDisplacement, }, }, - clipped: clip(existing.frame, displacedSubject), + clipped, }; // $ExpectError - using spread @@ -172,12 +179,7 @@ export const getDroppableDimension = ({ const subject: Area = addSpacing(withWindowScroll, margin); // use client + margin if frameClient is not provided - const frame: Area = (() => { - if (!frameClient) { - return subject; - } - return addPosition(frameClient, windowScroll); - })(); + const frame: ?Area = frameClient ? addPosition(frameClient, windowScroll) : null; const viewport: DroppableDimensionViewport = { frame, @@ -191,7 +193,7 @@ export const getDroppableDimension = ({ }, }, subject, - clipped: clip(frame, subject), + clipped: frame ? clip(frame, subject) : subject, }; const dimension: DroppableDimension = { diff --git a/src/state/get-droppable-over.js b/src/state/get-droppable-over.js index 87f3539dd0..fe681f8205 100644 --- a/src/state/get-droppable-over.js +++ b/src/state/get-droppable-over.js @@ -79,8 +79,7 @@ const getClippedAreaWithPlaceholder = ({ previousDroppableOverId && previousDroppableOverId === droppable.descriptor.id ); - const subject: Area = droppable.viewport.subject; - const frame: Area = droppable.viewport.frame; + const frame: ?Area = droppable.viewport.frame; const clipped: ?Area = droppable.viewport.clipped; // clipped area is totally hidden behind frame @@ -100,11 +99,10 @@ const getClippedAreaWithPlaceholder = ({ return clipped; } - const isClippedByFrame: boolean = subject[droppable.axis.size] !== frame[droppable.axis.size]; - const subjectWithGrowth = getWithGrowth(clipped, requiredGrowth); - if (!isClippedByFrame) { + // The droppable has no scroll container + if (!frame) { return subjectWithGrowth; } diff --git a/src/state/move-cross-axis/get-best-cross-axis-droppable.js b/src/state/move-cross-axis/get-best-cross-axis-droppable.js index 8293821a80..f450b6eac7 100644 --- a/src/state/move-cross-axis/get-best-cross-axis-droppable.js +++ b/src/state/move-cross-axis/get-best-cross-axis-droppable.js @@ -61,10 +61,15 @@ export default ({ // Remove any droppables that have invisible subjects .filter((droppable: DroppableDimension): boolean => Boolean(droppable.viewport.clipped)) // Remove any droppables that are not partially visible - .filter((droppable: DroppableDimension): boolean => ( - isVisibleThroughFrame(viewport)(droppable.viewport.frame) - )) - + .filter((droppable: DroppableDimension): boolean => { + // TODO: verify + const frame: ?Area = droppable.viewport.frame; + // Droppable has no scroll container + if (!frame) { + return true; + } + return isVisibleThroughFrame(viewport)(frame); + }) .filter((droppable: DroppableDimension): boolean => { const targetClipped: Area = getSafeClipped(droppable); diff --git a/src/types.js b/src/types.js index 144160475f..78e74e718b 100644 --- a/src/types.js +++ b/src/types.js @@ -101,7 +101,8 @@ export type DraggableDimension = {| export type DroppableDimensionViewport = {| // This is the window through which the droppable is observed // It does not change during a drag - frame: Area, + // frame is null when the droppable has no frame + frame: ?Area, // keeping track of the scroll frameScroll: {| initial: Position, From 3d9823fe3c89ac2933b2baa091a9e4d88dc80539 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 17 Jan 2018 19:29:01 +1100 Subject: [PATCH 002/163] initial --- .../auto-scroll-marshal-types.js | 6 ++ .../auto-scroll-marshal.js | 93 +++++++++++++++++++ src/state/create-store.js | 4 +- .../{ => debug-middleware}/log-middleware.js | 2 +- .../debug-middleware/timing-middleware.js | 14 +++ .../drag-drop-context/drag-drop-context.jsx | 8 ++ 6 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/state/auto-scroll-marshal/auto-scroll-marshal-types.js create mode 100644 src/state/auto-scroll-marshal/auto-scroll-marshal.js rename src/state/{ => debug-middleware}/log-middleware.js (87%) create mode 100644 src/state/debug-middleware/timing-middleware.js diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal-types.js b/src/state/auto-scroll-marshal/auto-scroll-marshal-types.js new file mode 100644 index 0000000000..a0017e7a06 --- /dev/null +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal-types.js @@ -0,0 +1,6 @@ +// @flow +import type { State } from '../../types'; + +export type AutoScrollMarshal = {| + onDrag: (state: State) => void, +|} diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js new file mode 100644 index 0000000000..9869aa5d75 --- /dev/null +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -0,0 +1,93 @@ +// @flow +import rafSchd from 'raf-schd'; +import getViewport from '../visibility/get-viewport'; +import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; +import type { + Area, + DroppableId, + DragState, + DraggableDimension, + DroppableDimension, + Position, + State, + Spacing, +} from '../../types'; + +type Args = {| + getClosestScrollable: (id: DroppableId) => ?HTMLElement +|} + +// returns null if no scroll is required +const getRequiredScroll = (container: Area, center: Position): ?Position => { + // get distance to each edge + const distance: Spacing = { + top: center.y - container.top, + right: container.right - center.x, + bottom: container.bottom - center.y, + left: center.x - container.left, + }; + + console.log(distance); + + return { x: 0, y: 0 }; +}; + +export default (): AutoScrollMarshal => { + // TODO: do not scroll if drag has finished + const scheduleScroll = rafSchd((change: Position) => { + console.log('scrolling window'); + window.scrollBy(change.x, change.y); + }); + + const onDrag = (state: State) => { + if (state.phase !== 'DRAGGING') { + console.error('Invalid phase for auto scrolling'); + return; + } + + const drag: ?DragState = state.drag; + + if (!drag) { + console.error('Invalid drag state'); + return; + } + + // const descriptor: DraggableDescriptor = drag.initial.descriptor; + const center: Position = drag.current.page.center; + // const draggable: DraggableDimension = state.dimension.draggable[descriptor.id]; + // const droppable: DroppableDimension = state.dimension.droppable[descriptor.droppableId]; + + // TODO: droppable scroll + + const viewport: Area = getViewport(); + + const requiredScroll: ?Position = getRequiredScroll(viewport, center); + + if (!requiredScroll) { + return; + } + + scheduleScroll(requiredScroll); + + // const bottomDiff = viewport.bottom - center.y; + // const topDiff = center.y - viewport.top; + + // console.log('top diff', topDiff); + + // if (bottomDiff < 100) { + // scheduleScroll({ x: 0, y: 20 }); + // return; + // } + + // if (topDiff < 100) { + // scheduleScroll({ x: 0, y: -20 }); + // } + }; + + const marshal: AutoScrollMarshal = { + onDrag, + }; + + return marshal; +}; + diff --git a/src/state/create-store.js b/src/state/create-store.js index c7aca34eda..0fb84d4399 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -18,7 +18,9 @@ export default (): Store => createStore( applyMiddleware( thunk, // debugging logger - // require('./log-middleware').default, + // require('./debug-middleware/log-middleware').default, + // debugging timer + // require('./debug-middleware/timing-middleware').default, ), ), ); diff --git a/src/state/log-middleware.js b/src/state/debug-middleware/log-middleware.js similarity index 87% rename from src/state/log-middleware.js rename to src/state/debug-middleware/log-middleware.js index 22db0159be..fc9184625c 100644 --- a/src/state/log-middleware.js +++ b/src/state/debug-middleware/log-middleware.js @@ -1,6 +1,6 @@ // @flow /* eslint-disable no-console */ -import type { Store, Action, State } from '../types'; +import type { Store, Action, State } from '../../types'; export default (store: Store) => (next: (Action) => mixed) => (action: Action): mixed => { console.group(`action: ${action.type}`); diff --git a/src/state/debug-middleware/timing-middleware.js b/src/state/debug-middleware/timing-middleware.js new file mode 100644 index 0000000000..b074235cf9 --- /dev/null +++ b/src/state/debug-middleware/timing-middleware.js @@ -0,0 +1,14 @@ +// @flow +/* eslint-disable no-console */ +import type { Action } from '../../types'; + +export default () => (next: (Action) => mixed) => (action: Action): mixed => { + const key = `action: ${action.type}`; + console.time(key); + + const result: mixed = next(action); + + console.timeEnd(key); + + return result; +}; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 546ef94716..03a7b67e39 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -6,6 +6,8 @@ import fireHooks from '../../state/fire-hooks'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; import createStyleMarshal from '../style-marshal/style-marshal'; import canStartDrag from '../../state/can-start-drag'; +import createAutoScroll from '../../state/auto-scroll-marshal/auto-scroll-marshal'; +import type { AutoScrollMarshal } from '../../state/auto-scroll-marshal/auto-scroll-marshal-types'; import type { StyleMarshal } from '../style-marshal/style-marshal-types'; import type { DimensionMarshal, @@ -108,6 +110,7 @@ export default class DragDropContext extends React.Component { }, }; this.dimensionMarshal = createDimensionMarshal(callbacks); + const scrollMarshal: AutoScrollMarshal = createAutoScroll(); let previous: State = this.store.getState(); @@ -118,7 +121,12 @@ export default class DragDropContext extends React.Component { // functions synchronously trigger more updates previous = current; + if (current.phase === 'DRAGGING') { + scrollMarshal.onDrag(current); + } + // no lifecycle changes have occurred if phase has not changed + if (current.phase === previousValue.phase) { return; } From 26405fd688a3514e855318df184535dd6bd7cd5c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 18 Jan 2018 15:51:57 +1100 Subject: [PATCH 003/163] basic scroll window down version --- .../auto-scroll-marshal-types.js | 2 +- .../auto-scroll-marshal.js | 66 +++++++++++++++++-- .../drag-drop-context/drag-drop-context.jsx | 42 ++++++------ 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal-types.js b/src/state/auto-scroll-marshal/auto-scroll-marshal-types.js index a0017e7a06..7c33b3ae4f 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal-types.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal-types.js @@ -2,5 +2,5 @@ import type { State } from '../../types'; export type AutoScrollMarshal = {| - onDrag: (state: State) => void, + onStateChange: (previous: State, current: State) => void, |} diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 9869aa5d75..ac301cace9 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -17,6 +17,13 @@ type Args = {| getClosestScrollable: (id: DroppableId) => ?HTMLElement |} +export const config = { + startDistance: 200, + distanceToEdgeForMaxSpeed: 50, + // pixels per frame + maxScrollSpeed: 24, +}; + // returns null if no scroll is required const getRequiredScroll = (container: Area, center: Position): ?Position => { // get distance to each edge @@ -27,15 +34,53 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { left: center.x - container.left, }; - console.log(distance); + // 1. Figure out which x,y values are the best target + // 2. Can the container scroll in that direction at all? + // If no for both directions, then return null + // 3. Is the center close enough to a edge to start a drag? + // 4. Based on the distance, calculate the speed at which a scroll should occur + // The lower distance value the faster the scroll should be. + // Maximum speed value should be hit before the distance is 0 + // Negative values to not continue to increase the speed + + // const vertical: number = (() => { + + // })(); + + // const horizontal: number = (() => { + + // }); + + // return { x: horizontal, y: vertical } + + // small enough distance to start drag + if (distance.bottom < config.startDistance) { + // the smaller the distance - the closer we move to the max scroll speed + // if we go into the negative distance - then we use the max scroll speed + + + + const diff: number = config.startDistance - distance.bottom; - return { x: 0, y: 0 }; + // going below the edge of the window + if (diff <= 0) { + return { x: 0, y: config.maxScrollSpeed }; + } + + const percentage: number = diff / config.startDistance; + const speed: number = config.maxScrollSpeed * percentage; + + return { x: 0, y: speed }; + } + + + return null; }; export default (): AutoScrollMarshal => { // TODO: do not scroll if drag has finished const scheduleScroll = rafSchd((change: Position) => { - console.log('scrolling window'); + console.log('scrolling window', change); window.scrollBy(change.x, change.y); }); @@ -84,8 +129,21 @@ export default (): AutoScrollMarshal => { // } }; + const onStateChange = (previous: State, current: State): void => { + // now dragging + if (current.phase === 'DRAGGING') { + onDrag(current); + return; + } + + // cancel any pending scrolls if no longer dragging + if (previous.phase === 'DRAGGING' && current.phase !== 'DRAGGING') { + scheduleScroll.cancel(); + } + }; + const marshal: AutoScrollMarshal = { - onDrag, + onStateChange, }; return marshal; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 03a7b67e39..e24d7c4069 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -51,6 +51,7 @@ export default class DragDropContext extends React.Component { store: Store dimensionMarshal: DimensionMarshal styleMarshal: StyleMarshal + scrollMarshal: AutoScrollMarshal unsubscribe: Function // Need to declare childContextTypes without flow @@ -110,7 +111,7 @@ export default class DragDropContext extends React.Component { }, }; this.dimensionMarshal = createDimensionMarshal(callbacks); - const scrollMarshal: AutoScrollMarshal = createAutoScroll(); + this.scrollMarshal = createAutoScroll(); let previous: State = this.store.getState(); @@ -121,30 +122,31 @@ export default class DragDropContext extends React.Component { // functions synchronously trigger more updates previous = current; - if (current.phase === 'DRAGGING') { - scrollMarshal.onDrag(current); - } - - // no lifecycle changes have occurred if phase has not changed + this.onStateChange(previousValue, current); - if (current.phase === previousValue.phase) { - return; + if (current.phase !== previousValue.phase) { + this.onPhaseChange(previous, current); } + }); + } - // Allowing dynamic hooks by re-capturing the hook functions - const hooks: Hooks = { - onDragStart: this.props.onDragStart, - onDragEnd: this.props.onDragEnd, - }; - fireHooks(hooks, previousValue, current); + onStateChange(previous: State, current: State) { + this.scrollMarshal.onStateChange(previous, current); + } + + onPhaseChange(previous: State, current: State) { + const hooks: Hooks = { + onDragStart: this.props.onDragStart, + onDragEnd: this.props.onDragEnd, + }; + fireHooks(hooks, previous, current); - // Update the global styles - this.styleMarshal.onPhaseChange(current); + // Update the global styles + this.styleMarshal.onPhaseChange(current); - // inform the dimension marshal about updates - // this can trigger more actions synchronously so we are placing it last - this.dimensionMarshal.onPhaseChange(current); - }); + // inform the dimension marshal about updates + // this can trigger more actions synchronously so we are placing it last + this.dimensionMarshal.onPhaseChange(current); } componentDidMount() { From 9fb48cbf80e32e172c181bbbfe2c5b6f0af7596e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 18 Jan 2018 16:16:45 +1100 Subject: [PATCH 004/163] wip --- .../auto-scroll-marshal.js | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index ac301cace9..93b0bfa08b 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -18,10 +18,11 @@ type Args = {| |} export const config = { - startDistance: 200, - distanceToEdgeForMaxSpeed: 50, + // pixels from the edge of a container for when an auto scroll starts + distanceToEdgeToStartScrolling: 250, + distanceToEdgeForMaxSpeed: 80, // pixels per frame - maxScrollSpeed: 24, + maxScrollSpeed: 10, }; // returns null if no scroll is required @@ -54,21 +55,39 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { // return { x: horizontal, y: vertical } // small enough distance to start drag - if (distance.bottom < config.startDistance) { + if (distance.bottom < config.distanceToEdgeToStartScrolling) { // the smaller the distance - the closer we move to the max scroll speed // if we go into the negative distance - then we use the max scroll speed + const scrollPlane: number = config.distanceToEdgeToStartScrolling - config.distanceToEdgeForMaxSpeed; - const diff: number = config.startDistance - distance.bottom; + // need to figure out the difference form the current position + const diff: number = config.distanceToEdgeToStartScrolling - distance.bottom; - // going below the edge of the window - if (diff <= 0) { + console.log('diff', diff); + + // going below the scroll plane + if (diff >= scrollPlane) { + console.log('using max scroll speed'); return { x: 0, y: config.maxScrollSpeed }; } - const percentage: number = diff / config.startDistance; + const percentage: number = diff / scrollPlane; const speed: number = config.maxScrollSpeed * percentage; + console.log('percentage', percentage); + + // console.log({ + // bottomDistance: distance.bottom, + // withBUffer: distance.bottom - config.distanceToEdgeForMaxSpeed, + // diff, + // diffBeforeCutoff: config.startDistance - config.distanceToEdgeForMaxSpeed, + // }) + + + + // const percentage: number = diff / config.startDistance; + // const speed: number = config.maxScrollSpeed * percentage; return { x: 0, y: speed }; } From 77e86865a3f9d4ef05fe2add9cb7bdb5ca6d10d9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 18 Jan 2018 16:24:45 +1100 Subject: [PATCH 005/163] fiddling with auto scroll for touch --- src/view/drag-handle/sensor/create-touch-sensor.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/view/drag-handle/sensor/create-touch-sensor.js b/src/view/drag-handle/sensor/create-touch-sensor.js index 5cf806133b..a9e19c42a5 100644 --- a/src/view/drag-handle/sensor/create-touch-sensor.js +++ b/src/view/drag-handle/sensor/create-touch-sensor.js @@ -176,10 +176,14 @@ export default ({ orientationchange: cancel, // some devices fire resize if the orientation changes resize: cancel, - // A window scroll will cancel a pending or current drag. - // This should not happen as we are calling preventDefault in touchmove, - // but just being extra safe - scroll: cancel, + scroll: () => { + // stop a pending drag + if (state.pending) { + stopPendingDrag(); + return; + } + schedule.windowScrollMove(); + }, // Long press can bring up a context menu // need to opt out of this behavior contextmenu: stopEvent, From 5eff7095409d83218bb19ce0c75d3ffcea6c4378 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 18 Jan 2018 16:56:21 +1100 Subject: [PATCH 006/163] wip --- src/state/auto-scroll-marshal/auto-scroll-marshal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 93b0bfa08b..027a31dda2 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -18,7 +18,7 @@ type Args = {| |} export const config = { - // pixels from the edge of a container for when an auto scroll starts + // TODO: should these percentages of the container size? distanceToEdgeToStartScrolling: 250, distanceToEdgeForMaxSpeed: 80, // pixels per frame From 317ed80cc35671c284db5091b0fd0e69142e3e89 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 19 Jan 2018 08:21:17 +1100 Subject: [PATCH 007/163] prototype of window scroll working --- .../auto-scroll-marshal.js | 93 ++++++++++--------- .../drag-drop-context/drag-drop-context.jsx | 8 +- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 027a31dda2..b209f45b8b 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -1,6 +1,7 @@ // @flow import rafSchd from 'raf-schd'; import getViewport from '../visibility/get-viewport'; +import { isEqual } from '../position'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; import type { Area, @@ -19,10 +20,36 @@ type Args = {| export const config = { // TODO: should these percentages of the container size? - distanceToEdgeToStartScrolling: 250, - distanceToEdgeForMaxSpeed: 80, + distanceToEdgeToStartScrolling: 300, + distanceToEdgeForMaxSpeed: 100, // pixels per frame - maxScrollSpeed: 10, + maxScrollSpeed: 30, +}; + +const origin: Position = { x: 0, y: 0 }; + +const getSpeed = (distance: number): number => { + if (distance > config.distanceToEdgeToStartScrolling) { + return 0; + } + + // Okay, we need to scroll + + const scrollPlane: number = config.distanceToEdgeToStartScrolling - config.distanceToEdgeForMaxSpeed; + + // need to figure out the difference form the current position + const diff: number = config.distanceToEdgeToStartScrolling - distance; + + // going below the scroll plane + if (diff >= scrollPlane) { + console.log('using max scroll speed'); + return config.maxScrollSpeed; + } + + const percentage: number = diff / scrollPlane; + const speed: number = config.maxScrollSpeed * percentage; + + return speed; }; // returns null if no scroll is required @@ -44,62 +71,38 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { // Maximum speed value should be hit before the distance is 0 // Negative values to not continue to increase the speed - // const vertical: number = (() => { - - // })(); + const vertical: number = (() => { + const isCloserToBottom: boolean = distance.bottom < distance.top; - // const horizontal: number = (() => { - - // }); - - // return { x: horizontal, y: vertical } - - // small enough distance to start drag - if (distance.bottom < config.distanceToEdgeToStartScrolling) { - // the smaller the distance - the closer we move to the max scroll speed - // if we go into the negative distance - then we use the max scroll speed - - const scrollPlane: number = config.distanceToEdgeToStartScrolling - config.distanceToEdgeForMaxSpeed; - - - // need to figure out the difference form the current position - const diff: number = config.distanceToEdgeToStartScrolling - distance.bottom; - - console.log('diff', diff); - - // going below the scroll plane - if (diff >= scrollPlane) { - console.log('using max scroll speed'); - return { x: 0, y: config.maxScrollSpeed }; + if (isCloserToBottom) { + return getSpeed(distance.bottom); } - const percentage: number = diff / scrollPlane; - const speed: number = config.maxScrollSpeed * percentage; - console.log('percentage', percentage); - - // console.log({ - // bottomDistance: distance.bottom, - // withBUffer: distance.bottom - config.distanceToEdgeForMaxSpeed, - // diff, - // diffBeforeCutoff: config.startDistance - config.distanceToEdgeForMaxSpeed, - // }) + // closer to top + return -1 * getSpeed(distance.top); + })(); + const horizontal: number = (() => { + const isCloserToRight: boolean = distance.right < distance.left; + if (isCloserToRight) { + return getSpeed(distance.right); + } - // const percentage: number = diff / config.startDistance; - // const speed: number = config.maxScrollSpeed * percentage; - - return { x: 0, y: speed }; - } + // closer to left + return -1 * getSpeed(distance.left); + })(); + const scroll: Position = { x: horizontal, y: vertical }; - return null; + return isEqual(scroll, origin) ? null : scroll; }; export default (): AutoScrollMarshal => { // TODO: do not scroll if drag has finished const scheduleScroll = rafSchd((change: Position) => { console.log('scrolling window', change); + // TODO: check if can actually scroll this much window.scrollBy(change.x, change.y); }); diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index e24d7c4069..df453b9868 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -122,11 +122,13 @@ export default class DragDropContext extends React.Component { // functions synchronously trigger more updates previous = current; - this.onStateChange(previousValue, current); - if (current.phase !== previousValue.phase) { - this.onPhaseChange(previous, current); + // executing phase change handlers first + this.onPhaseChange(previousValue, current); } + + // TODO: should this take the latest previous to prevent scroll post drop? + this.onStateChange(previousValue, current); }); } From e2825b6680b57a1966bed1a0b145f85520c87e25 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 19 Jan 2018 09:05:04 +1100 Subject: [PATCH 008/163] fiddling --- src/state/auto-scroll-marshal/auto-scroll-marshal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index b209f45b8b..343ea87a1d 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -20,8 +20,8 @@ type Args = {| export const config = { // TODO: should these percentages of the container size? - distanceToEdgeToStartScrolling: 300, - distanceToEdgeForMaxSpeed: 100, + distanceToEdgeToStartScrolling: 250, + distanceToEdgeForMaxSpeed: 20, // pixels per frame maxScrollSpeed: 30, }; From 2d99b9de76023a01810bad3aaa11e15ff220acf8 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 19 Jan 2018 15:50:24 +1100 Subject: [PATCH 009/163] wip --- .../auto-scroll-marshal.js | 134 ++++++++++++------ .../dimension-marshal-types.js | 3 + .../dimension-marshal/dimension-marshal.js | 14 ++ .../drag-drop-context/drag-drop-context.jsx | 4 +- .../droppable-dimension-publisher.jsx | 10 ++ stories/3-board-story.js | 4 +- 6 files changed, 119 insertions(+), 50 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 343ea87a1d..113171c16d 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -2,11 +2,14 @@ import rafSchd from 'raf-schd'; import getViewport from '../visibility/get-viewport'; import { isEqual } from '../position'; +import { vertical, horizontal } from '../axis'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; import type { Area, + Axis, DroppableId, DragState, + DraggableLocation, DraggableDimension, DroppableDimension, Position, @@ -15,38 +18,61 @@ import type { } from '../../types'; type Args = {| - getClosestScrollable: (id: DroppableId) => ?HTMLElement + scrollDroppable: (id: DroppableId, change: Position) => void, |} export const config = { - // TODO: should these percentages of the container size? - distanceToEdgeToStartScrolling: 250, - distanceToEdgeForMaxSpeed: 20, + // percentage distance from edge of container: + startFrom: 0.18, + maxSpeedAt: 0.05, // pixels per frame - maxScrollSpeed: 30, + maxScrollSpeed: 25, }; const origin: Position = { x: 0, y: 0 }; -const getSpeed = (distance: number): number => { - if (distance > config.distanceToEdgeToStartScrolling) { +type Thresholds = {| + startFrom: number, + maxSpeedAt: number, + accelerationPlane: number, +|} + +const getThresholds = (container: Area, axis: Axis): Thresholds => { + const startFrom: number = container[axis.size] * config.startFrom; + const maxSpeedAt: number = container[axis.size] * config.maxSpeedAt; + const accelerationPlane: number = startFrom - maxSpeedAt; + + const thresholds: Thresholds = { + startFrom, + maxSpeedAt, + accelerationPlane, + }; + + return thresholds; +}; + +const getSpeed = (distance: number, thresholds: Thresholds): number => { + // Not close enough to the edge + if (distance >= thresholds.startFrom) { return 0; } - // Okay, we need to scroll - - const scrollPlane: number = config.distanceToEdgeToStartScrolling - config.distanceToEdgeForMaxSpeed; + // gone past the edge (currently not supported) + // TODO: do not want for window - but need it for droppable? + // if (distance < 0) { + // return 0; + // } - // need to figure out the difference form the current position - const diff: number = config.distanceToEdgeToStartScrolling - distance; + // Already past the maxSpeedAt point - // going below the scroll plane - if (diff >= scrollPlane) { - console.log('using max scroll speed'); + if (distance <= thresholds.maxSpeedAt) { return config.maxScrollSpeed; } - const percentage: number = diff / scrollPlane; + // We need to perform a scroll as a percentage of the max scroll speed + + const distancePastStart: number = thresholds.startFrom - distance; + const percentage: number = distancePastStart / thresholds.accelerationPlane; const speed: number = config.maxScrollSpeed * percentage; return speed; @@ -71,41 +97,45 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { // Maximum speed value should be hit before the distance is 0 // Negative values to not continue to increase the speed - const vertical: number = (() => { + const y: number = (() => { + const thresholds: Thresholds = getThresholds(container, vertical); const isCloserToBottom: boolean = distance.bottom < distance.top; if (isCloserToBottom) { - return getSpeed(distance.bottom); + return getSpeed(distance.bottom, thresholds); } // closer to top - return -1 * getSpeed(distance.top); + return -1 * getSpeed(distance.top, thresholds); })(); - const horizontal: number = (() => { + const x: number = (() => { + const thresholds: Thresholds = getThresholds(container, horizontal); const isCloserToRight: boolean = distance.right < distance.left; if (isCloserToRight) { - return getSpeed(distance.right); + return getSpeed(distance.right, thresholds); } // closer to left - return -1 * getSpeed(distance.left); + return -1 * getSpeed(distance.left, thresholds); })(); - const scroll: Position = { x: horizontal, y: vertical }; + const scroll: Position = { x, y }; return isEqual(scroll, origin) ? null : scroll; }; -export default (): AutoScrollMarshal => { +export default ({ + scrollDroppable, +}: Args): AutoScrollMarshal => { // TODO: do not scroll if drag has finished - const scheduleScroll = rafSchd((change: Position) => { - console.log('scrolling window', change); - // TODO: check if can actually scroll this much + const scheduleWindowScroll = rafSchd((change: Position) => { window.scrollBy(change.x, change.y); }); + const scheduleDroppableScroll = rafSchd(scrollDroppable); + const onDrag = (state: State) => { if (state.phase !== 'DRAGGING') { console.error('Invalid phase for auto scrolling'); @@ -119,36 +149,45 @@ export default (): AutoScrollMarshal => { return; } - // const descriptor: DraggableDescriptor = drag.initial.descriptor; const center: Position = drag.current.page.center; - // const draggable: DraggableDimension = state.dimension.draggable[descriptor.id]; - // const droppable: DroppableDimension = state.dimension.droppable[descriptor.droppableId]; - // TODO: droppable scroll + const wasDroppableScrolled: boolean = (() => { + const destination: ?DraggableLocation = drag.impact.destination; - const viewport: Area = getViewport(); + if (!destination) { + return false; + } - const requiredScroll: ?Position = getRequiredScroll(viewport, center); + const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; - if (!requiredScroll) { + // not a scrollable droppable + if (!droppable.viewport.frame) { + return false; + } + + const requiredScroll: ?Position = getRequiredScroll(droppable.viewport.frame, center); + + if (!requiredScroll) { + return false; + } + + scheduleDroppableScroll(droppable.descriptor.id, requiredScroll); + return true; + })(); + + if (wasDroppableScrolled) { return; } - scheduleScroll(requiredScroll); + // Now we check to see if we need to scroll the viewport - // const bottomDiff = viewport.bottom - center.y; - // const topDiff = center.y - viewport.top; + const requiredScroll: ?Position = getRequiredScroll(getViewport(), center); - // console.log('top diff', topDiff); - - // if (bottomDiff < 100) { - // scheduleScroll({ x: 0, y: 20 }); - // return; - // } + if (!requiredScroll) { + return; + } - // if (topDiff < 100) { - // scheduleScroll({ x: 0, y: -20 }); - // } + scheduleWindowScroll(requiredScroll); }; const onStateChange = (previous: State, current: State): void => { @@ -160,7 +199,8 @@ export default (): AutoScrollMarshal => { // cancel any pending scrolls if no longer dragging if (previous.phase === 'DRAGGING' && current.phase !== 'DRAGGING') { - scheduleScroll.cancel(); + scheduleWindowScroll.cancel(); + scheduleDroppableScroll.cancel(); } }; diff --git a/src/state/dimension-marshal/dimension-marshal-types.js b/src/state/dimension-marshal/dimension-marshal-types.js index cc6ff72e22..5f17049a5d 100644 --- a/src/state/dimension-marshal/dimension-marshal-types.js +++ b/src/state/dimension-marshal/dimension-marshal-types.js @@ -15,6 +15,8 @@ export type GetDroppableDimensionFn = () => DroppableDimension; export type DroppableCallbacks = {| getDimension: GetDroppableDimensionFn, + // scroll a droppable + scroll: (change: Position) => void, // Droppable must listen to scroll events and publish them using the // onChange callback. If the Droppable is not in a scroll container then // it does not need to do anything @@ -56,6 +58,7 @@ export type DimensionMarshal = {| // it is possible for a droppable to change whether it is enabled during a drag updateDroppableIsEnabled: (id: DroppableId, isEnabled: boolean) => void, updateDroppableScroll: (id: DroppableId, newScroll: Position) => void, + scrollDroppable: (id: DroppableId, change: Position) => void, unregisterDroppable: (descriptor: DroppableDescriptor) => void, // Entry onPhaseChange: (current: State) => void, diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index d65c793679..c6e6f6378c 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -156,6 +156,19 @@ export default (callbacks: Callbacks) => { callbacks.updateDroppableScroll(id, newScroll); }; + const scrollDroppable = (id: DroppableId, change: Position) => { + const entry: ?DroppableEntry = state.droppables[id]; + if (!entry) { + return; + } + + if (!state.isCollecting) { + return; + } + + entry.callbacks.scroll(change); + }; + const unregisterDraggable = (descriptor: DraggableDescriptor) => { const entry: ?DraggableEntry = state.draggables[descriptor.id]; @@ -435,6 +448,7 @@ export default (callbacks: Callbacks) => { registerDroppable, unregisterDroppable, updateDroppableIsEnabled, + scrollDroppable, updateDroppableScroll, onPhaseChange, }; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index df453b9868..5f1f42aaff 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -111,7 +111,9 @@ export default class DragDropContext extends React.Component { }, }; this.dimensionMarshal = createDimensionMarshal(callbacks); - this.scrollMarshal = createAutoScroll(); + this.scrollMarshal = createAutoScroll({ + scrollDroppable: this.dimensionMarshal.scrollDroppable, + }); let previous: State = this.store.getState(); diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index beb5927563..1813a9e546 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -50,6 +50,7 @@ export default class DroppableDimensionPublisher extends Component { getDimension: this.getDimension, watchScroll: this.watchScroll, unwatchScroll: this.unwatchScroll, + scroll: this.scroll, }; this.callbacks = callbacks; } @@ -93,6 +94,15 @@ export default class DroppableDimensionPublisher extends Component { this.scheduleScrollUpdate(this.getScrollOffset()); } + scroll = (change: Position) => { + if (this.closestScrollable == null) { + return; + } + + this.closestScrollable.scrollTop += change.y; + this.closestScrollable.scrollLeft += change.x; + } + watchScroll = () => { if (!this.props.targetRef) { console.error('cannot watch droppable scroll if not in the dom'); diff --git a/stories/3-board-story.js b/stories/3-board-story.js index f8fbd7aaa4..4d0358a2fe 100644 --- a/stories/3-board-story.js +++ b/stories/3-board-story.js @@ -5,7 +5,7 @@ import Board from './src/board/board'; import { authorQuoteMap, generateQuoteMap } from './src/data'; const data = { - medium: generateQuoteMap(50), + medium: generateQuoteMap(100), large: generateQuoteMap(500), }; @@ -17,5 +17,5 @@ storiesOf('board', module) )) .add('long lists in a short container', () => ( - + )); From 9bf1100b0dc33e012b069d5f5930a33d6cbb41bb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 19 Jan 2018 17:07:04 +1100 Subject: [PATCH 010/163] detecting frame over --- .../auto-scroll-marshal.js | 18 +++++-- .../get-droppable-frame-over.js | 49 +++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 src/state/auto-scroll-marshal/get-droppable-frame-over.js diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 113171c16d..5e79c9447b 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -3,6 +3,7 @@ import rafSchd from 'raf-schd'; import getViewport from '../visibility/get-viewport'; import { isEqual } from '../position'; import { vertical, horizontal } from '../axis'; +import getDroppableFrameOver from './get-droppable-frame-over'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; import type { Area, @@ -151,16 +152,23 @@ export default ({ const center: Position = drag.current.page.center; + // TODO: + // improvements: need to find if we are over any FRAME - not just the droppable + // window scroll first? + const wasDroppableScrolled: boolean = (() => { - const destination: ?DraggableLocation = drag.impact.destination; + const droppable: ?DroppableDimension = getDroppableFrameOver({ + target: center, + droppables: state.dimension.droppable, + }); + + console.log('over frame', droppable && droppable.descriptor.id); - if (!destination) { + if (!droppable) { return false; } - const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; - - // not a scrollable droppable + // not a scrollable droppable (should not occur) if (!droppable.viewport.frame) { return false; } diff --git a/src/state/auto-scroll-marshal/get-droppable-frame-over.js b/src/state/auto-scroll-marshal/get-droppable-frame-over.js new file mode 100644 index 0000000000..a77ea51f65 --- /dev/null +++ b/src/state/auto-scroll-marshal/get-droppable-frame-over.js @@ -0,0 +1,49 @@ +// @flow +import memoizeOne from 'memoize-one'; +import isPositionInFrame from '../visibility/is-position-in-frame'; +import type { + Area, + DroppableDimension, + DroppableDimensionMap, + DroppableId, + Position, +} from '../../types'; + +type Args = {| + target: Position, + droppables: DroppableDimensionMap, +|}; + +const getDroppablesWithAFrame = memoizeOne( + (droppables: DroppableDimensionMap): DroppableDimension[] => ( + Object.keys(droppables) + .map((id: DroppableId): DroppableDimension => droppables[id]) + .filter((droppable: DroppableDimension): boolean => { + // exclude disabled droppables + if (!droppable.isEnabled) { + return false; + } + + // only want droppables that have a frame + if (!droppable.viewport.frame) { + return false; + } + + return true; + }) + ) +); + +export default ({ + target, + droppables, +}: Args): ?DroppableDimension => { + const overDroppablesFrame: ?DroppableDimension = + getDroppablesWithAFrame(droppables) + .find((droppable: DroppableDimension): boolean => { + const frame: Area = (droppable.viewport.frame: any); + return isPositionInFrame(frame)(target); + }); + + return overDroppablesFrame; +}; From 18e4a1acbdf9c30787b1121ff8679ceb4c095a21 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 22 Jan 2018 10:19:31 +1100 Subject: [PATCH 011/163] wip --- .../auto-scroll-marshal.js | 60 ++++++++++--------- .../auto-scroll-marshal/scroll-window.js | 49 +++++++++++++++ src/state/create-store.js | 2 +- src/view/drag-handle/drag-handle-types.js | 2 +- .../drag-handle/sensor/create-mouse-sensor.js | 1 + 5 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 src/state/auto-scroll-marshal/scroll-window.js diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 5e79c9447b..b4e4b76e07 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -4,6 +4,7 @@ import getViewport from '../visibility/get-viewport'; import { isEqual } from '../position'; import { vertical, horizontal } from '../axis'; import getDroppableFrameOver from './get-droppable-frame-over'; +import scrollWindow from './scroll-window'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; import type { Area, @@ -131,9 +132,7 @@ export default ({ scrollDroppable, }: Args): AutoScrollMarshal => { // TODO: do not scroll if drag has finished - const scheduleWindowScroll = rafSchd((change: Position) => { - window.scrollBy(change.x, change.y); - }); + const scheduleWindowScroll = rafSchd(scrollWindow); const scheduleDroppableScroll = rafSchd(scrollDroppable); @@ -153,44 +152,47 @@ export default ({ const center: Position = drag.current.page.center; // TODO: - // improvements: need to find if we are over any FRAME - not just the droppable - // window scroll first? + // Need to see if we can drag the droppable (or window if it is first) + // and if we cannot scroll then move on - const wasDroppableScrolled: boolean = (() => { - const droppable: ?DroppableDimension = getDroppableFrameOver({ - target: center, - droppables: state.dimension.droppable, - }); + // const wasDroppableScrolled: boolean = (() => { + // const droppable: ?DroppableDimension = getDroppableFrameOver({ + // target: center, + // droppables: state.dimension.droppable, + // }); - console.log('over frame', droppable && droppable.descriptor.id); + // console.log('over frame', droppable && droppable.descriptor.id); - if (!droppable) { - return false; - } + // if (!droppable) { + // return false; + // } - // not a scrollable droppable (should not occur) - if (!droppable.viewport.frame) { - return false; - } + // // not a scrollable droppable (should not occur) + // if (!droppable.viewport.frame) { + // return false; + // } - const requiredScroll: ?Position = getRequiredScroll(droppable.viewport.frame, center); + // const requiredScroll: ?Position = getRequiredScroll(droppable.viewport.frame, center); - if (!requiredScroll) { - return false; - } + // if (!requiredScroll) { + // return false; + // } - scheduleDroppableScroll(droppable.descriptor.id, requiredScroll); - return true; - })(); + // scheduleDroppableScroll(droppable.descriptor.id, requiredScroll); + // return true; + // })(); - if (wasDroppableScrolled) { - return; - } + // if (wasDroppableScrolled) { + // return; + // } // Now we check to see if we need to scroll the viewport - const requiredScroll: ?Position = getRequiredScroll(getViewport(), center); + const viewport: Area = getViewport(); + + const requiredScroll: ?Position = getRequiredScroll(viewport, center); + // No scroll required if (!requiredScroll) { return; } diff --git a/src/state/auto-scroll-marshal/scroll-window.js b/src/state/auto-scroll-marshal/scroll-window.js new file mode 100644 index 0000000000..78a8a659eb --- /dev/null +++ b/src/state/auto-scroll-marshal/scroll-window.js @@ -0,0 +1,49 @@ +// @flow +import { offset } from '../spacing'; +import getViewport from '../visibility/get-viewport'; +import type { + Area, + Position, + Spacing, +} from '../../types'; + +// Will return true if can scroll even a little bit in either direction +// of the change. +const canScroll = (change: Position): boolean => { + const viewport: Area = getViewport(); + + const shifted: Spacing = offset(viewport, change); + + // TEMP + // if (shifted.left === 0 && shifted.top === 0) { + // return true; + // } + + // moving back beyond origin + if (shifted.left <= 0 && shifted.top <= 0) { + return false; + } + + const el: ?HTMLElement = document.documentElement; + + if (!el) { + console.error('Cannot find document element'); + return false; + } + + // totally outside the full height of the page + if (shifted.right >= el.scrollWidth && shifted.bottom >= el.scrollHeight) { + return false; + } + + return true; +}; + +export default (change: Position): void => { + if (canScroll(change)) { + window.scrollBy(change.x, change.y); + } else { + console.log('cannot scroll window!', change); + } +}; + diff --git a/src/state/create-store.js b/src/state/create-store.js index 0fb84d4399..459d6dd1ae 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -18,7 +18,7 @@ export default (): Store => createStore( applyMiddleware( thunk, // debugging logger - // require('./debug-middleware/log-middleware').default, + require('./debug-middleware/log-middleware').default, // debugging timer // require('./debug-middleware/timing-middleware').default, ), diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/drag-handle/drag-handle-types.js index dd42b884ff..5b18e55684 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/drag-handle/drag-handle-types.js @@ -5,7 +5,7 @@ import type { Position, Direction, DraggableId } from '../../types'; export type Callbacks = {| onLift: ({ client: Position, isScrollAllowed: boolean }) => void, onMove: (point: Position) => void, - onWindowScroll: (diff: Position) => void, + onWindowScroll: () => void, onMoveForward: () => void, onMoveBackward: () => void, onCrossAxisMoveForward: () => void, diff --git a/src/view/drag-handle/sensor/create-mouse-sensor.js b/src/view/drag-handle/sensor/create-mouse-sensor.js index 1d62820e13..db21fa2ce1 100644 --- a/src/view/drag-handle/sensor/create-mouse-sensor.js +++ b/src/view/drag-handle/sensor/create-mouse-sensor.js @@ -175,6 +175,7 @@ export default ({ eventKeys.forEach((eventKey: string) => { if (eventKey === 'scroll') { + // eventual consistency is fine because we use position: fixed on the item win.addEventListener(eventKey, windowBindings.scroll, { passive: true }); return; } From db199bd4b5f4c655ed86b355c558fa894e906eb4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 23 Jan 2018 14:57:08 +1100 Subject: [PATCH 012/163] as good as we can do --- README.md | 2 +- src/state/create-store.js | 2 +- src/state/reducer.js | 8 +++++++- src/view/drag-handle/sensor/create-touch-sensor.js | 8 ++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a469dade69..70deafa1b5 100644 --- a/README.md +++ b/README.md @@ -537,7 +537,7 @@ type Hooks = {| type Props = {| ...Hooks, - children?: ReactElement, + children: ?Node, |} ``` diff --git a/src/state/create-store.js b/src/state/create-store.js index 459d6dd1ae..0fb84d4399 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -18,7 +18,7 @@ export default (): Store => createStore( applyMiddleware( thunk, // debugging logger - require('./debug-middleware/log-middleware').default, + // require('./debug-middleware/log-middleware').default, // debugging timer // require('./debug-middleware/timing-middleware').default, ), diff --git a/src/state/reducer.js b/src/state/reducer.js index d18f6d1296..982f6eb156 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -23,7 +23,7 @@ import type { Position, InitialDragPositions, } from '../types'; -import { add, subtract } from './position'; +import { add, subtract, isEqual } from './position'; import { noMovement } from './no-impact'; import getDragImpact from './get-drag-impact/'; import moveToNextIndex from './move-to-next-index/'; @@ -412,6 +412,12 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } + if (isEqual(windowScroll, state.drag.current.windowScroll)) { + // TODO: remove warn + console.warn('not computing move by window scroll as it is unchanged'); + return state; + } + return move({ state, clientSelection: state.drag.current.client.selection, diff --git a/src/view/drag-handle/sensor/create-touch-sensor.js b/src/view/drag-handle/sensor/create-touch-sensor.js index 9a535faa88..a50e3fdbd9 100644 --- a/src/view/drag-handle/sensor/create-touch-sensor.js +++ b/src/view/drag-handle/sensor/create-touch-sensor.js @@ -224,6 +224,14 @@ export default ({ return; } + // For scroll events we are okay with eventual consistency. + // Passive scroll listeners is the default behavior for mobile + // but we are being really clear here + if (eventKey === 'scroll') { + win.addEventListener(eventKey, fn, { passive: true }); + return; + } + win.addEventListener(eventKey, fn); }); }; From a8993effdc94fb32a3b1a2e7bf25c1cc501fc1c5 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 23 Jan 2018 15:36:15 +1100 Subject: [PATCH 013/163] uncommenting droppable code --- .../auto-scroll-marshal.js | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index b4e4b76e07..0c1c8252a3 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -155,36 +155,36 @@ export default ({ // Need to see if we can drag the droppable (or window if it is first) // and if we cannot scroll then move on - // const wasDroppableScrolled: boolean = (() => { - // const droppable: ?DroppableDimension = getDroppableFrameOver({ - // target: center, - // droppables: state.dimension.droppable, - // }); + const wasDroppableScrolled: boolean = (() => { + const droppable: ?DroppableDimension = getDroppableFrameOver({ + target: center, + droppables: state.dimension.droppable, + }); - // console.log('over frame', droppable && droppable.descriptor.id); + console.log('over frame', droppable && droppable.descriptor.id); - // if (!droppable) { - // return false; - // } + if (!droppable) { + return false; + } - // // not a scrollable droppable (should not occur) - // if (!droppable.viewport.frame) { - // return false; - // } + // not a scrollable droppable (should not occur) + if (!droppable.viewport.frame) { + return false; + } - // const requiredScroll: ?Position = getRequiredScroll(droppable.viewport.frame, center); + const requiredScroll: ?Position = getRequiredScroll(droppable.viewport.frame, center); - // if (!requiredScroll) { - // return false; - // } + if (!requiredScroll) { + return false; + } - // scheduleDroppableScroll(droppable.descriptor.id, requiredScroll); - // return true; - // })(); + scheduleDroppableScroll(droppable.descriptor.id, requiredScroll); + return true; + })(); - // if (wasDroppableScrolled) { - // return; - // } + if (wasDroppableScrolled) { + return; + } // Now we check to see if we need to scroll the viewport From 47962de3f478ac9b311133e8c4ef7d9bac9a6354 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 24 Jan 2018 11:40:11 +1100 Subject: [PATCH 014/163] scroll handover --- .../auto-scroll-marshal.js | 60 +++++++------------ .../auto-scroll-marshal/scroll-window.js | 2 +- src/state/reducer.js | 8 +-- .../droppable-dimension-publisher.jsx | 6 +- 4 files changed, 31 insertions(+), 45 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 0c1c8252a3..6124548839 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -4,15 +4,13 @@ import getViewport from '../visibility/get-viewport'; import { isEqual } from '../position'; import { vertical, horizontal } from '../axis'; import getDroppableFrameOver from './get-droppable-frame-over'; -import scrollWindow from './scroll-window'; +import scrollWindow, { canScroll as canScrollWindow } from './scroll-window'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; import type { Area, Axis, DroppableId, DragState, - DraggableLocation, - DraggableDimension, DroppableDimension, Position, State, @@ -133,7 +131,6 @@ export default ({ }: Args): AutoScrollMarshal => { // TODO: do not scroll if drag has finished const scheduleWindowScroll = rafSchd(scrollWindow); - const scheduleDroppableScroll = rafSchd(scrollDroppable); const onDrag = (state: State) => { @@ -151,53 +148,42 @@ export default ({ const center: Position = drag.current.page.center; - // TODO: - // Need to see if we can drag the droppable (or window if it is first) - // and if we cannot scroll then move on - - const wasDroppableScrolled: boolean = (() => { - const droppable: ?DroppableDimension = getDroppableFrameOver({ - target: center, - droppables: state.dimension.droppable, - }); - - console.log('over frame', droppable && droppable.descriptor.id); + // Ideally we would - if (!droppable) { - return false; - } + // 1. Can we scroll the viewport? - // not a scrollable droppable (should not occur) - if (!droppable.viewport.frame) { - return false; - } + const viewport: Area = getViewport(); + const requiredWindowScroll: ?Position = getRequiredScroll(viewport, center); - const requiredScroll: ?Position = getRequiredScroll(droppable.viewport.frame, center); + if (requiredWindowScroll && canScrollWindow(requiredWindowScroll)) { + scheduleWindowScroll(requiredWindowScroll); + return; + } - if (!requiredScroll) { - return false; - } + // 2. We are not scrolling the window. Can we scroll the Droppable? - scheduleDroppableScroll(droppable.descriptor.id, requiredScroll); - return true; - })(); + const droppable: ?DroppableDimension = getDroppableFrameOver({ + target: center, + droppables: state.dimension.droppable, + }); - if (wasDroppableScrolled) { + if (!droppable) { return; } - // Now we check to see if we need to scroll the viewport - - const viewport: Area = getViewport(); + // not a scrollable droppable (should not occur) + if (!droppable.viewport.frame) { + return; + } - const requiredScroll: ?Position = getRequiredScroll(viewport, center); + const requiredFrameScroll: ?Position = + getRequiredScroll(droppable.viewport.frame, center); - // No scroll required - if (!requiredScroll) { + if (!requiredFrameScroll) { return; } - scheduleWindowScroll(requiredScroll); + scheduleDroppableScroll(droppable.descriptor.id, requiredFrameScroll); }; const onStateChange = (previous: State, current: State): void => { diff --git a/src/state/auto-scroll-marshal/scroll-window.js b/src/state/auto-scroll-marshal/scroll-window.js index 78a8a659eb..f1bbe84beb 100644 --- a/src/state/auto-scroll-marshal/scroll-window.js +++ b/src/state/auto-scroll-marshal/scroll-window.js @@ -9,7 +9,7 @@ import type { // Will return true if can scroll even a little bit in either direction // of the change. -const canScroll = (change: Position): boolean => { +export const canScroll = (change: Position): boolean => { const viewport: Area = getViewport(); const shifted: Spacing = offset(viewport, change); diff --git a/src/state/reducer.js b/src/state/reducer.js index 982f6eb156..618944d458 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -412,15 +412,15 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } - if (isEqual(windowScroll, state.drag.current.windowScroll)) { - // TODO: remove warn - console.warn('not computing move by window scroll as it is unchanged'); + const current: CurrentDrag = state.drag.current; + + if (isEqual(windowScroll, current.windowScroll)) { return state; } return move({ state, - clientSelection: state.drag.current.client.selection, + clientSelection: current.client.selection, windowScroll, shouldAnimate: false, }); diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 1813a9e546..89c8e9d969 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -59,7 +59,7 @@ export default class DroppableDimensionPublisher extends Component { [dimensionMarshalKey]: PropTypes.object.isRequired, }; - getScrollOffset = (): Position => { + getClosestScroll = (): Position => { if (!this.closestScrollable) { return origin; } @@ -91,7 +91,7 @@ export default class DroppableDimensionPublisher extends Component { }); onClosestScroll = () => { - this.scheduleScrollUpdate(this.getScrollOffset()); + this.scheduleScrollUpdate(this.getClosestScroll()); } scroll = (change: Position) => { @@ -233,7 +233,7 @@ export default class DroppableDimensionPublisher extends Component { // side effect - grabbing it for scroll listening so we know it is the same node this.closestScrollable = getClosestScrollable(targetRef); - const frameScroll: Position = this.getScrollOffset(); + const frameScroll: Position = this.getClosestScroll(); const style: Object = window.getComputedStyle(targetRef); // keeping it simple and always using the margin of the droppable From 2742d41527a94becefbea324b630e940a45d404b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 24 Jan 2018 16:56:31 +1100 Subject: [PATCH 015/163] minor --- src/state/auto-scroll-marshal/auto-scroll-marshal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 6124548839..a968a97cbf 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -121,9 +121,9 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { return -1 * getSpeed(distance.left, thresholds); })(); - const scroll: Position = { x, y }; + const required: Position = { x, y }; - return isEqual(scroll, origin) ? null : scroll; + return isEqual(required, origin) ? null : required; }; export default ({ From 97cd95b65955049f354b446d3bdb873f0fe76bc3 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 25 Jan 2018 08:10:19 +1100 Subject: [PATCH 016/163] adding DroppableProps type --- README.md | 34 ++++++++++++++----- src/index.js | 1 + src/view/droppable/droppable-types.js | 6 ++++ src/view/droppable/droppable.jsx | 22 +++++++++++- src/view/style-marshal/get-styles.js | 33 +++++++++++++++--- stories/src/board/board.jsx | 2 +- .../interactive-elements-app.jsx | 1 + stories/src/primatives/author-list.jsx | 2 +- stories/src/primatives/quote-list.jsx | 1 + stories/src/vertical-nested/quote-list.jsx | 1 + .../integration/hooks-integration.spec.js | 2 +- .../integration/server-side-rendering.spec.js | 2 +- test/unit/view/connected-droppable.spec.js | 2 +- test/unit/view/drag-drop-context.spec.js | 4 +-- 14 files changed, 93 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 70deafa1b5..de723dd9ed 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ class App extends Component {
{this.state.items.map((item, index) => ( @@ -699,6 +700,7 @@ import { Droppable } from 'react-beautiful-dnd';

I am a droppable!

{provided.placeholder} @@ -734,18 +736,30 @@ The function is provided with two arguments: ```js type DroppableProvided = {| innerRef: (?HTMLElement) => void, + droppableProps: DroppableProps, placeholder: ?ReactElement, |} ``` -- `provided.innerRef`: In order for the droppable to function correctly, **you must** bind the `provided.innerRef` to the highest possible DOM node in the `ReactElement`. We do this in order to avoid needing to use `ReactDOM` to look up your DOM node. - +- `provided.innerRef`: In order for the droppable to function correctly, **you must** bind the `provided.innerRef` to the highest possible DOM node in the `ReactElement`. We do this in order to avoid needing to use `ReactDOM` to look up your DOM node. *This prop is planned to be removed when we move to React 16* - `provided.placeholder`: This is used to create space in the `Droppable` as needed during a drag. This space is needed when a user is dragging over a list that is not the home list. Please be sure to put the placeholder inside of the component for which you have provided the ref. We need to increase the side of the `Droppable` itself. This is different from `Draggable` where the `placeholder` needs to be a *sibling* to the draggable node. +- `provided.droppableProps (DroppableProps)`: This is an Object that contains properties that need to be applied to a Droppable element. It needs to be applied to the same element that you apply `provided.innerRef` to. It currently contains a `data` attribute that we use to control some non-visible css. + +#### Type information + +```js +// Props that can be spread onto the element directly +export type DroppableProps = {| + // used for shared global styles + 'data-react-beautiful-dnd-droppable': string, +|} +``` + ```js {(provided, snapshot) => ( -
+
Good to go {provided.placeholder} @@ -770,6 +784,7 @@ The `children` function is also provided with a small amount of state relating t
I am a droppable! @@ -838,6 +853,7 @@ class Students extends Component {
{provided.placeholder} @@ -923,8 +939,10 @@ The function is provided with two arguments: ```js type DraggableProvided = {| innerRef: (HTMLElement) => void, - draggableProps: ?DraggableProps, + draggableProps: DraggableProps, + // will be null if the draggable is disabled dragHandleProps: ?DragHandleProps, + // null if not required placeholder: ?ReactElement, |} ``` @@ -939,15 +957,15 @@ Everything within the *provided* object must be applied for the `Draggable` to f ; ``` -### Type information +#### Type information ```js innerRef: (HTMLElement) => void ``` -- `provided.draggableProps (?DraggableProps)`: This is an Object that contains a `data` attribute and an inline style. This Object needs to be applied to the same node that you apply `provided.innerRef` to. This controls the movement of the draggable when it is dragging and not dragging. You are welcome to add your own styles to `DraggableProps > style` – but please do not remove or replace any of the properties. +- `provided.draggableProps (DraggableProps)`: This is an Object that contains a `data` attribute and an inline `style`. This Object needs to be applied to the same node that you apply `provided.innerRef` to. This controls the movement of the draggable when it is dragging and not dragging. You are welcome to add your own styles to `DraggableProps > style` – but please do not remove or replace any of the properties. -### Type information +#### Type information ```js // Props that can be spread onto the element directly @@ -1290,7 +1308,7 @@ type DraggableStateSnapshot = {| // Draggable type DraggableProvided = {| innerRef: (?HTMLElement) => void, - draggableProps: ?DraggableProps, + draggableProps: DraggableProps, dragHandleProps: ?DragHandleProps, placeholder: ?ReactElement, |} diff --git a/src/index.js b/src/index.js index 049b645063..6562477118 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ export type { export type { Provided as DroppableProvided, StateSnapshot as DroppableStateSnapshot, + DroppableProps, } from './view/droppable/droppable-types'; // Draggable diff --git a/src/view/droppable/droppable-types.js b/src/view/droppable/droppable-types.js index f815f15fcb..0e11225dca 100644 --- a/src/view/droppable/droppable-types.js +++ b/src/view/droppable/droppable-types.js @@ -7,9 +7,15 @@ import type { Placeholder, } from '../../types'; +export type DroppableProps = {| + // used for shared global styles + 'data-react-beautiful-dnd-droppable': string, +|} + export type Provided = {| innerRef: (?HTMLElement) => void, placeholder: ?Node, + droppableProps: DroppableProps, |} export type StateSnapshot = {| diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 1c9320ff37..8be049cd9f 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -5,7 +5,10 @@ import type { Props, Provided, StateSnapshot, DefaultProps } from './droppable-t import type { DroppableId } from '../../types'; import DroppableDimensionPublisher from '../droppable-dimension-publisher/'; import Placeholder from '../placeholder/'; -import { droppableIdKey } from '../context-keys'; +import { + droppableIdKey, + styleContextKey, +} from '../context-keys'; type State = {| ref: ?HTMLElement, @@ -17,6 +20,8 @@ type Context = {| export default class Droppable extends Component { /* eslint-disable react/sort-comp */ + styleContext: string + state: State = { ref: null, } @@ -28,6 +33,17 @@ export default class Droppable extends Component { ignoreContainerClipping: false, } + // Need to declare childContextTypes without flow + static contextTypes = { + [styleContextKey]: PropTypes.string.isRequired, + } + + constructor(props: Props, context: Object) { + super(props, context); + + this.styleContext = context[styleContextKey]; + } + // Need to declare childContextTypes without flow // https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22 static childContextTypes = { @@ -40,6 +56,7 @@ export default class Droppable extends Component { }; return value; } + /* eslint-enable */ // React calls ref callback twice for every render @@ -83,6 +100,9 @@ export default class Droppable extends Component { const provided: Provided = { innerRef: this.setRef, placeholder: this.getPlaceholder(), + droppableProps: { + 'data-react-beautiful-dnd-droppable': this.styleContext, + }, }; const snapshot: StateSnapshot = { isDraggingOver, diff --git a/src/view/style-marshal/get-styles.js b/src/view/style-marshal/get-styles.js index 4cea955a02..48895ac94d 100644 --- a/src/view/style-marshal/get-styles.js +++ b/src/view/style-marshal/get-styles.js @@ -13,6 +13,7 @@ const prefix: string = 'data-react-beautiful-dnd'; export default (styleContext: string): Styles => { const dragHandleSelector: string = `[${prefix}-drag-handle="${styleContext}"]`; const draggableSelector: string = `[${prefix}-draggable="${styleContext}"]`; + const droppableSelector: string = `[${prefix}-droppable="${styleContext}"]`; // ## Drag handle styles @@ -87,6 +88,25 @@ export default (styleContext: string): Styles => { `, }; + // ## Droppable styles + + // ### Base + // > Applied at all times + + // overflow-anchor: none; + // Opting out of the browser feature which tries to maintain + // the scroll position when the DOM changes above the fold. + // This does not work well with reordering DOM nodes. + // When we drop a Draggable it already has the correct scroll applied. + + const droppableStyles = { + base: ` + ${droppableSelector} { + overflow-anchor: none; + } + `, + }; + // ## Body styles // ### While active dragging @@ -112,20 +132,25 @@ export default (styleContext: string): Styles => { `, }; - const resting: string = [ + const base: string[] = [ dragHandleStyles.base, + droppableStyles.base, + ]; + + const resting: string = [ + ...base, dragHandleStyles.grabCursor, ].join(''); const dragging: string = [ - dragHandleStyles.base, + ...base, dragHandleStyles.blockPointerEvents, draggableStyles.animateMovement, bodyStyles.whileActiveDragging, ].join(''); const dropAnimating: string = [ - dragHandleStyles.base, + ...base, dragHandleStyles.grabCursor, draggableStyles.animateMovement, ].join(''); @@ -133,7 +158,7 @@ export default (styleContext: string): Styles => { // Not applying grab cursor during a cancel as it is not possible for users to reorder // items during a cancel const userCancel: string = [ - dragHandleStyles.base, + ...base, draggableStyles.animateMovement, ].join(''); diff --git a/stories/src/board/board.jsx b/stories/src/board/board.jsx index fa23e13159..e71a55e0db 100644 --- a/stories/src/board/board.jsx +++ b/stories/src/board/board.jsx @@ -120,7 +120,7 @@ export default class Board extends Component { ignoreContainerClipping={Boolean(containerHeight)} > {(provided: DroppableProvided) => ( - + {ordered.map((key: string, index: number) => ( { {(droppableProvided: DroppableProvided) => ( {this.state.items.map((item: ItemType, index: number) => ( { return ( {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( - + {internalScroll ? ( {this.renderBoard(dropProvided)} diff --git a/stories/src/primatives/quote-list.jsx b/stories/src/primatives/quote-list.jsx index 438390f575..ca8581141a 100644 --- a/stories/src/primatives/quote-list.jsx +++ b/stories/src/primatives/quote-list.jsx @@ -148,6 +148,7 @@ export default class QuoteList extends Component { style={style} isDraggingOver={dropSnapshot.isDraggingOver} isDropDisabled={isDropDisabled} + {...dropProvided.droppableProps} > {internalScroll ? ( diff --git a/stories/src/vertical-nested/quote-list.jsx b/stories/src/vertical-nested/quote-list.jsx index c3e41716ba..b5e8cb56c2 100644 --- a/stories/src/vertical-nested/quote-list.jsx +++ b/stories/src/vertical-nested/quote-list.jsx @@ -71,6 +71,7 @@ export default class QuoteList extends Component<{ list: NestedQuoteList }> { {list.title} {list.children.map((item: Quote | NestedQuoteList, index: number) => ( diff --git a/test/unit/integration/hooks-integration.spec.js b/test/unit/integration/hooks-integration.spec.js index ab893d545d..42a10bd311 100644 --- a/test/unit/integration/hooks-integration.spec.js +++ b/test/unit/integration/hooks-integration.spec.js @@ -61,7 +61,7 @@ describe('hooks integration', () => { > {(droppableProvided: DroppableProvided) => ( -
+

Droppable

{(draggableProvided: DraggableProvided) => ( diff --git a/test/unit/integration/server-side-rendering.spec.js b/test/unit/integration/server-side-rendering.spec.js index b3a3eb55b8..1407c80761 100644 --- a/test/unit/integration/server-side-rendering.spec.js +++ b/test/unit/integration/server-side-rendering.spec.js @@ -25,7 +25,7 @@ class App extends Component<*, *> { > {(provided: DroppableProvided) => ( -
+
{(dragProvided: DraggableProvided) => (
diff --git a/test/unit/view/connected-droppable.spec.js b/test/unit/view/connected-droppable.spec.js index 92bb51f510..235fc9ff4e 100644 --- a/test/unit/view/connected-droppable.spec.js +++ b/test/unit/view/connected-droppable.spec.js @@ -395,7 +395,7 @@ describe('Connected Droppable', () => { render() { const { provided, name } = this.props; return ( -
provided.innerRef(ref)}> +
provided.innerRef(ref)} {...provided.droppableProps}> hello {name}
); diff --git a/test/unit/view/drag-drop-context.spec.js b/test/unit/view/drag-drop-context.spec.js index 49551c9338..5c96a698aa 100644 --- a/test/unit/view/drag-drop-context.spec.js +++ b/test/unit/view/drag-drop-context.spec.js @@ -105,7 +105,7 @@ describe('DragDropContext', () => { { }}> {(droppableProvided: DroppableProvided) => ( -
+
{(draggableProvided: DraggableProvided) => (
@@ -141,7 +141,7 @@ describe('DragDropContext', () => { { }}> {(droppableProvided: DroppableProvided) => ( -
+
{(draggableProvided: DraggableProvided) => (
From aa20b591762b2350cd239fb69d3c3dc7c286ea3b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 25 Jan 2018 10:57:13 +1100 Subject: [PATCH 017/163] fixing incorrect droppable offset due to scroll on drop --- .../droppable-dimension-publisher.jsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 89c8e9d969..30b759e0a9 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -83,16 +83,12 @@ export default class DroppableDimensionPublisher extends Component { marshal.updateDroppableScroll(this.publishedDescriptor.id, newScroll); }); - scheduleScrollUpdate = rafSchedule((offset: Position) => { - // might no longer be listening for scroll changes by the time a frame comes back - if (this.isWatchingScroll) { - this.memoizedUpdateScroll(offset.x, offset.y); - } + scheduleScrollUpdate = rafSchedule(() => { + const offset: Position = this.getClosestScroll(); + this.memoizedUpdateScroll(offset.x, offset.y); }); - onClosestScroll = () => { - this.scheduleScrollUpdate(this.getClosestScroll()); - } + onClosestScroll = () => this.scheduleScrollUpdate(); scroll = (change: Position) => { if (this.closestScrollable == null) { @@ -130,6 +126,7 @@ export default class DroppableDimensionPublisher extends Component { } this.isWatchingScroll = false; + this.scheduleScrollUpdate.cancel(); if (!this.closestScrollable) { console.error('cannot unbind event listener if element is null'); From c5cef97bacfdaf180f3335d92d4ea4ae6bbbf497 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 25 Jan 2018 10:59:18 +1100 Subject: [PATCH 018/163] adding comment --- .../droppable-dimension-publisher.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 30b759e0a9..45c3bb2094 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -84,6 +84,7 @@ export default class DroppableDimensionPublisher extends Component { }); scheduleScrollUpdate = rafSchedule(() => { + // Capturing the scroll now so that it is the latest value const offset: Position = this.getClosestScroll(); this.memoizedUpdateScroll(offset.x, offset.y); }); From 5338ad403652b111d3e3747d21275d5e6897ff28 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 25 Jan 2018 21:31:26 +1100 Subject: [PATCH 019/163] progress --- src/state/action-creators.js | 13 ++-- .../auto-scroll-marshal.js | 73 ++++++++++++++++--- .../auto-scroll-marshal/scroll-window.js | 21 +++++- src/state/get-displacement.js | 2 +- .../get-best-cross-axis-droppable.js | 5 +- .../move-cross-axis/get-closest-draggable.js | 2 +- .../move-to-next-index/in-foreign-list.js | 4 +- src/state/move-to-next-index/in-home-list.js | 32 +++++--- ... => is-totally-visible-in-new-location.js} | 11 ++- .../move-to-next-index-types.js | 5 ++ src/state/reducer.js | 27 ++++--- ... => is-partially-visible-through-frame.js} | 0 .../is-totally-visible-through-frame.js | 20 +++++ ...{is-partially-visible.js => is-visible.js} | 42 +++++++++-- src/types.js | 10 ++- src/view/drag-handle/drag-handle-types.js | 9 ++- .../sensor/create-keyboard-sensor.js | 14 +++- .../drag-handle/sensor/create-mouse-sensor.js | 5 +- .../drag-handle/sensor/create-touch-sensor.js | 3 +- src/view/draggable/draggable.jsx | 7 +- .../visibility/is-partially-visible.spec.js | 2 +- 21 files changed, 235 insertions(+), 72 deletions(-) rename src/state/move-to-next-index/{is-visible-in-new-location.js => is-totally-visible-in-new-location.js} (67%) rename src/state/visibility/{is-visible-through-frame.js => is-partially-visible-through-frame.js} (100%) create mode 100644 src/state/visibility/is-totally-visible-through-frame.js rename src/state/visibility/{is-partially-visible.js => is-visible.js} (54%) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index e2430907a8..087c12aec5 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -15,6 +15,7 @@ import type { CurrentDrag, InitialDrag, DraggableDescriptor, + AutoScrollMode, } from '../types'; import noImpact from './no-impact'; import getNewHomeClientCenter from './get-new-home-client-center'; @@ -61,7 +62,7 @@ export type CompleteLiftAction = {| id: DraggableId, client: InitialDragPositions, windowScroll: Position, - isScrollAllowed: boolean, + autoScrollMode: AutoScrollMode, |} |} @@ -69,14 +70,14 @@ export const completeLift = ( id: DraggableId, client: InitialDragPositions, windowScroll: Position, - isScrollAllowed: boolean, + autoScrollMode: AutoScrollMode, ): CompleteLiftAction => ({ type: 'COMPLETE_LIFT', payload: { id, client, windowScroll, - isScrollAllowed, + autoScrollMode, }, }); @@ -429,7 +430,7 @@ export type LiftAction = {| id: DraggableId, client: InitialDragPositions, windowScroll: Position, - isScrollAllowed: boolean, + autoScrollMode: AutoScrollMode, |} |} @@ -438,7 +439,7 @@ export type LiftAction = {| export const lift = (id: DraggableId, client: InitialDragPositions, windowScroll: Position, - isScrollAllowed: boolean, + autoScrollMode: AutoScrollMode, ) => (dispatch: Dispatch, getState: Function) => { // Phase 1: Quickly finish any current drop animations const initial: State = getState(); @@ -481,7 +482,7 @@ export const lift = (id: DraggableId, return; } - dispatch(completeLift(id, client, windowScroll, isScrollAllowed)); + dispatch(completeLift(id, client, windowScroll, autoScrollMode)); }); }); }; diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index a968a97cbf..f8263fb28e 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -15,6 +15,8 @@ import type { Position, State, Spacing, + DraggableLocation, + DraggableDimension, } from '../../types'; type Args = {| @@ -126,6 +128,9 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { return isEqual(required, origin) ? null : required; }; +const isTooBigForAutoScrolling = (frame: Area, subject: Area): boolean => + subject.width > frame.width || subject.height > frame.height; + export default ({ scrollDroppable, }: Args): AutoScrollMarshal => { @@ -133,14 +138,8 @@ export default ({ const scheduleWindowScroll = rafSchd(scrollWindow); const scheduleDroppableScroll = rafSchd(scrollDroppable); - const onDrag = (state: State) => { - if (state.phase !== 'DRAGGING') { - console.error('Invalid phase for auto scrolling'); - return; - } - + const fluidScroll = (state: State) => { const drag: ?DragState = state.drag; - if (!drag) { console.error('Invalid drag state'); return; @@ -148,11 +147,15 @@ export default ({ const center: Position = drag.current.page.center; - // Ideally we would - // 1. Can we scroll the viewport? + const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; const viewport: Area = getViewport(); + + if (isTooBigForAutoScrolling(viewport, draggable.page.withMargin)) { + return; + } + const requiredWindowScroll: ?Position = getRequiredScroll(viewport, center); if (requiredWindowScroll && canScrollWindow(requiredWindowScroll)) { @@ -186,11 +189,59 @@ export default ({ scheduleDroppableScroll(droppable.descriptor.id, requiredFrameScroll); }; + const jumpScroll = (state: State) => { + const drag: DragState = (state.drag: any); + const offset: Position = (drag.scrollJumpRequest : any); + + const destination: ?DraggableLocation = drag.impact.destination; + + if (!destination) { + console.error('Cannot perform a jump scroll when there is no destination'); + return; + } + + const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; + const viewport: Area = getViewport(); + + // draggable is too big + if (isTooBigForAutoScrolling(viewport, draggable.page.withMargin)) { + return; + } + + if (canScrollWindow(offset)) { + scrollWindow(offset); + return; + } + console.log('cannot scroll window!', offset); + + const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; + + // scroll the droppable if it is a scroll container + if (droppable.viewport.frame) { + scrollDroppable(destination.droppableId, offset); + } + }; + const onStateChange = (previous: State, current: State): void => { // now dragging if (current.phase === 'DRAGGING') { - onDrag(current); - return; + if (!current.drag) { + console.error('invalid drag state'); + return; + } + + if (current.drag.initial.autoScrollMode === 'FLUID') { + fluidScroll(current); + return; + } + + // autoScrollMode == 'JUMP' + + if (!current.drag.scrollJumpRequest) { + return; + } + + jumpScroll(current); } // cancel any pending scrolls if no longer dragging diff --git a/src/state/auto-scroll-marshal/scroll-window.js b/src/state/auto-scroll-marshal/scroll-window.js index f1bbe84beb..1e6c7f97cb 100644 --- a/src/state/auto-scroll-marshal/scroll-window.js +++ b/src/state/auto-scroll-marshal/scroll-window.js @@ -7,12 +7,30 @@ import type { Spacing, } from '../../types'; +const getSmallestSignedValue = (value: number) => { + if (value === 0) { + return 0; + } + return value > 0 ? 1 : -1; +}; + +type Args = {| + draggable: DraggableDimension, + change: Position, + viewport: Area, +|} + // Will return true if can scroll even a little bit in either direction // of the change. export const canScroll = (change: Position): boolean => { const viewport: Area = getViewport(); + // Only need to be able to move the smallest amount in the desired direction + const smallestChange: Position = { + x: getSmallestSignedValue(change.x), + y: getSmallestSignedValue(change.y), + }; - const shifted: Spacing = offset(viewport, change); + const shifted: Spacing = offset(viewport, smallestChange); // TEMP // if (shifted.left === 0 && shifted.top === 0) { @@ -39,6 +57,7 @@ export const canScroll = (change: Position): boolean => { return true; }; +// Not guarenteed to scroll by the entire amount export default (change: Position): void => { if (canScroll(change)) { window.scrollBy(change.x, change.y); diff --git a/src/state/get-displacement.js b/src/state/get-displacement.js index ae17fc9702..80d7e16b0e 100644 --- a/src/state/get-displacement.js +++ b/src/state/get-displacement.js @@ -1,6 +1,6 @@ // @flow import getDisplacementMap, { type DisplacementMap } from './get-displacement-map'; -import isPartiallyVisible from './visibility/is-partially-visible'; +import { isPartiallyVisible } from './visibility/is-visible'; import type { DraggableId, Displacement, diff --git a/src/state/move-cross-axis/get-best-cross-axis-droppable.js b/src/state/move-cross-axis/get-best-cross-axis-droppable.js index f450b6eac7..e8b0233930 100644 --- a/src/state/move-cross-axis/get-best-cross-axis-droppable.js +++ b/src/state/move-cross-axis/get-best-cross-axis-droppable.js @@ -3,7 +3,7 @@ import { closest } from '../position'; import isWithin from '../is-within'; import { getCorners } from '../spacing'; import getViewport from '../visibility/get-viewport'; -import isVisibleThroughFrame from '../visibility/is-visible-through-frame'; +import isPartiallyVisibleThroughFrame from '../visibility/is-partially-visible-through-frame'; import type { Axis, DroppableDimension, @@ -62,13 +62,12 @@ export default ({ .filter((droppable: DroppableDimension): boolean => Boolean(droppable.viewport.clipped)) // Remove any droppables that are not partially visible .filter((droppable: DroppableDimension): boolean => { - // TODO: verify const frame: ?Area = droppable.viewport.frame; // Droppable has no scroll container if (!frame) { return true; } - return isVisibleThroughFrame(viewport)(frame); + return isPartiallyVisibleThroughFrame(viewport)(frame); }) .filter((droppable: DroppableDimension): boolean => { const targetClipped: Area = getSafeClipped(droppable); diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index 1514c3b753..00b5da4261 100644 --- a/src/state/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-cross-axis/get-closest-draggable.js @@ -1,7 +1,7 @@ // @flow import { distance } from '../position'; import getViewport from '../visibility/get-viewport'; -import isPartiallyVisible from '../visibility/is-partially-visible'; +import { isPartiallyVisible } from '../visibility/is-visible'; import type { Area, Axis, diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index 83f38e16f5..ee2ffc7230 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -4,7 +4,7 @@ import { patch } from '../position'; import moveToEdge from '../move-to-edge'; import getDisplacement from '../get-displacement'; import getViewport from '../visibility/get-viewport'; -import isVisibleInNewLocation from './is-visible-in-new-location'; +import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import type { @@ -91,7 +91,7 @@ export default ({ // checking the shifted draggable rather than just the new center // as the new center might not be visible but the whole draggable // might be partially visible - return isVisibleInNewLocation({ + return isTotallyVisibleInNewLocation({ draggable, destination: droppable, newCenter, diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 413cd5f5d2..2ba4c8bdbe 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,8 +1,7 @@ // @flow -import memoizeOne from 'memoize-one'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch } from '../position'; -import isVisibleInNewLocation from './is-visible-in-new-location'; +import { patch, subtract } from '../position'; +import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import getViewport from '../visibility/get-viewport'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; @@ -18,15 +17,10 @@ import type { Area, } from '../../types'; -const getIndex = memoizeOne( - (draggables: DraggableDimension[], - target: DraggableDimension - ): number => draggables.indexOf(target) -); - export default ({ isMovingForward, draggableId, + previousPageCenter, previousImpact, droppable, draggables, @@ -46,7 +40,7 @@ export default ({ draggables, ); - const startIndex: number = getIndex(insideDroppable, draggable); + const startIndex: number = draggable.descriptor.index; const currentIndex: number = location.index; const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; @@ -88,7 +82,7 @@ export default ({ const viewport: Area = getViewport(); - const isVisible: boolean = isVisibleInNewLocation({ + const isVisible: boolean = isTotallyVisibleInNewLocation({ draggable, destination: droppable, newCenter, @@ -96,7 +90,20 @@ export default ({ }); if (!isVisible) { - return null; + // HACK! just experimenting + // const diff: Position = + // need to get diff from where we are right now + const diff: Position = subtract(newCenter, previousPageCenter); + + const result: Result = { + pageCenter: previousPageCenter, + impact: previousImpact, + scrollJumpRequest: diff, + }; + // console.log('diff', diff); + + // window.scrollBy(diff.x, diff.y); + return result; } // Calculate DragImpact @@ -145,6 +152,7 @@ export default ({ const result: Result = { pageCenter: newCenter, impact: newImpact, + scrollJumpRequest: null, }; return result; diff --git a/src/state/move-to-next-index/is-visible-in-new-location.js b/src/state/move-to-next-index/is-totally-visible-in-new-location.js similarity index 67% rename from src/state/move-to-next-index/is-visible-in-new-location.js rename to src/state/move-to-next-index/is-totally-visible-in-new-location.js index c07a71f47d..9fd1654b43 100644 --- a/src/state/move-to-next-index/is-visible-in-new-location.js +++ b/src/state/move-to-next-index/is-totally-visible-in-new-location.js @@ -1,7 +1,8 @@ // @flow import { subtract } from '../position'; import { offset } from '../spacing'; -import isPartiallyVisible from '../visibility/is-partially-visible'; +import { isTotallyVisible } from '../visibility/is-visible'; +import isTotallyVisibleThroughFrame from '../visibility/is-totally-visible-through-frame'; import type { Area, DraggableDimension, @@ -27,9 +28,15 @@ export default ({ const diff: Position = subtract(newCenter, draggable.page.withMargin.center); const shifted: Spacing = offset(draggable.page.withMargin, diff); - return isPartiallyVisible({ + // Must be totally visible, not just partially visible. + + const isVisible: boolean = isTotallyVisible({ target: shifted, destination, viewport, }); + + console.log('is totally visible?', isVisible); + + return isVisible; }; diff --git a/src/state/move-to-next-index/move-to-next-index-types.js b/src/state/move-to-next-index/move-to-next-index-types.js index 9f3f9dcd89..3c60dccae1 100644 --- a/src/state/move-to-next-index/move-to-next-index-types.js +++ b/src/state/move-to-next-index/move-to-next-index-types.js @@ -10,6 +10,7 @@ import type { export type Args = {| isMovingForward: boolean, draggableId: DraggableId, + previousPageCenter: Position, previousImpact: DragImpact, droppable: DroppableDimension, draggables: DraggableDimensionMap, @@ -20,4 +21,8 @@ export type Result = {| pageCenter: Position, // the impact of the movement impact: DragImpact, + // Any scroll that is required for the movement. + // If this is present then the pageCenter and impact + // will just be the same as the previous drag + scrollJumpRequest: ?Position, |} diff --git a/src/state/reducer.js b/src/state/reducer.js index 618944d458..b3bc083c70 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -119,6 +119,7 @@ const move = ({ initial, impact: newImpact, current, + scrollJumpRequest: null, }; return { @@ -243,7 +244,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { return state; } - const { id, client, windowScroll, isScrollAllowed } = action.payload; + const { id, client, windowScroll, autoScrollMode } = action.payload; const page: InitialDragPositions = { selection: add(client.selection, windowScroll), center: add(client.center, windowScroll), @@ -260,7 +261,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { const initial: InitialDrag = { descriptor, - isScrollAllowed, + autoScrollMode, client, page, windowScroll, @@ -308,6 +309,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { initial, current, impact, + scrollJumpRequest: null, }, }; } @@ -323,14 +325,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } - // Currently not supporting container scrolling while dragging with a keyboard - // We do not store whether we are dragging with a keyboard in the state but this flag - // does this trick. Ideally this check would not exist. - // Kill the drag instantly - if (!state.drag.initial.isScrollAllowed) { - return clean(); - } - const { id, offset } = action.payload; const target: ?DroppableDimension = state.dimension.droppable[id]; @@ -454,6 +448,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { draggableId: existing.initial.descriptor.id, droppable, draggables: state.dimension.draggable, + previousPageCenter: existing.current.page.center, previousImpact: existing.impact, }); @@ -462,6 +457,18 @@ export default (state: State = clean('IDLE'), action: Action): State => { return state; } + // requesting a scroll jump + if (result.scrollJumpRequest) { + return { + ...state, + phase: 'DRAGGING', + drag: { + ...existing, + scrollJumpRequest: result.scrollJumpRequest, + }, + }; + } + const impact: DragImpact = result.impact; const page: Position = result.pageCenter; const client: Position = subtract(page, existing.current.windowScroll); diff --git a/src/state/visibility/is-visible-through-frame.js b/src/state/visibility/is-partially-visible-through-frame.js similarity index 100% rename from src/state/visibility/is-visible-through-frame.js rename to src/state/visibility/is-partially-visible-through-frame.js diff --git a/src/state/visibility/is-totally-visible-through-frame.js b/src/state/visibility/is-totally-visible-through-frame.js new file mode 100644 index 0000000000..a37d680966 --- /dev/null +++ b/src/state/visibility/is-totally-visible-through-frame.js @@ -0,0 +1,20 @@ +// @flow +import isWithin from '../is-within'; +import type { + Spacing, +} from '../../types'; + +export default (frame: Spacing) => { + const isWithinVertical = isWithin(frame.top, frame.bottom); + const isWithinHorizontal = isWithin(frame.left, frame.right); + + return (subject: Spacing) => { + const isContained: boolean = + isWithinVertical(subject.top) && + isWithinVertical(subject.bottom) && + isWithinHorizontal(subject.left) && + isWithinHorizontal(subject.right); + + return isContained; + }; +}; diff --git a/src/state/visibility/is-partially-visible.js b/src/state/visibility/is-visible.js similarity index 54% rename from src/state/visibility/is-partially-visible.js rename to src/state/visibility/is-visible.js index af40960462..6b85d29cc4 100644 --- a/src/state/visibility/is-partially-visible.js +++ b/src/state/visibility/is-visible.js @@ -1,5 +1,6 @@ // @flow -import isVisibleThroughFrame from './is-visible-through-frame'; +import isPartiallyVisibleThroughFrame from './is-partially-visible-through-frame'; +import isTotallyVisibleThroughFrame from './is-totally-visible-through-frame'; import { offset } from '../spacing'; import type { Spacing, @@ -14,14 +15,17 @@ type Args = {| viewport: Area, |} -// will return true if the position is visible: -// 1. within the viewport AND -// 2. within the destination Droppable -export default ({ +type HelperArgs = {| + ...Args, + isVisibleThroughFrameFn: (frame: Spacing) => (subject: Spacing) => boolean +|} + +const isVisible = ({ target, destination, viewport, -}: Args): boolean => { + isVisibleThroughFrameFn, +}: HelperArgs): boolean => { const displacement: Position = destination.viewport.frameScroll.diff.displacement; const withScroll: Spacing = offset(target, displacement); @@ -36,12 +40,34 @@ export default ({ // adjust for the scroll as the clipped viewport takes into account // the scroll of the droppable. const isVisibleInDroppable: boolean = - isVisibleThroughFrame(destination.viewport.clipped)(withScroll); + isVisibleThroughFrameFn(destination.viewport.clipped)(withScroll); // We also need to consider whether the destination scroll when detecting // if we are visible in the viewport. const isVisibleInViewport: boolean = - isVisibleThroughFrame(viewport)(withScroll); + isVisibleThroughFrameFn(viewport)(withScroll); return isVisibleInDroppable && isVisibleInViewport; }; + +export const isPartiallyVisible = ({ + target, + destination, + viewport, +}: Args): boolean => isVisible({ + target, + destination, + viewport, + isVisibleThroughFrameFn: isPartiallyVisibleThroughFrame, +}); + +export const isTotallyVisible = ({ + target, + destination, + viewport, +}: Args): boolean => isVisible({ + target, + destination, + viewport, + isVisibleThroughFrameFn: isTotallyVisibleThroughFrame, +}); diff --git a/src/types.js b/src/types.js index 78e74e718b..f880f2878e 100644 --- a/src/types.js +++ b/src/types.js @@ -183,10 +183,14 @@ export type InitialDragPositions = {| center: Position, |} +// When dragging with a pointer such as a mouse or touch input we want to automatically +// scroll user the under input when we get near the bottom of a Droppable or the window. +// When Dragging with a keyboard we want to jump as required +export type AutoScrollMode = 'FLUID' | 'JUMP'; + export type InitialDrag = {| descriptor: DraggableDescriptor, - // whether scrolling is allowed - otherwise a scroll will cancel the drag - isScrollAllowed: boolean, + autoScrollMode: AutoScrollMode, // relative to the viewport when the drag started client: InitialDragPositions, // viewport + window scroll (position relative to 0, 0) @@ -237,6 +241,8 @@ export type DropResult = {| export type DragState = {| initial: InitialDrag, current: CurrentDrag, + // if we need to jump the scroll - how much we need to jump + scrollJumpRequest: ?Position, impact: DragImpact, |} diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/drag-handle/drag-handle-types.js index 5b18e55684..4e7f400390 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/drag-handle/drag-handle-types.js @@ -1,9 +1,14 @@ // @flow import type { Node } from 'react'; -import type { Position, Direction, DraggableId } from '../../types'; +import type { + AutoScrollMode, + Position, + Direction, + DraggableId, +} from '../../types'; export type Callbacks = {| - onLift: ({ client: Position, isScrollAllowed: boolean }) => void, + onLift: ({ client: Position, autoScrollMode: AutoScrollMode }) => void, onMove: (point: Position) => void, onWindowScroll: () => void, onMoveForward: () => void, diff --git a/src/view/drag-handle/sensor/create-keyboard-sensor.js b/src/view/drag-handle/sensor/create-keyboard-sensor.js index c60c5c245a..1cffbd7c53 100644 --- a/src/view/drag-handle/sensor/create-keyboard-sensor.js +++ b/src/view/drag-handle/sensor/create-keyboard-sensor.js @@ -82,8 +82,10 @@ export default ({ // using center position as selection const center: Position = getCenterPosition(ref); - // not allowing scrolling with a mouse when lifting with a keyboard - startDragging(() => callbacks.onLift({ client: center, isScrollAllowed: false })); + startDragging(() => callbacks.onLift({ + client: center, + autoScrollMode: 'JUMP', + })); return; } @@ -162,8 +164,12 @@ export default ({ // any mouse down kills a drag mousedown: cancel, resize: cancel, - // currently not supporting window scrolling with a keyboard - scroll: cancel, + // Cancel if the user is using the mouse wheel + // We are not supporting wheel / trackpad scrolling with keyboard dragging + wheel: cancel, + // Need to respond instantly to a jump scroll request + // Not using the scheduler + scroll: callbacks.onWindowScroll, }; const eventKeys: string[] = Object.keys(windowBindings); diff --git a/src/view/drag-handle/sensor/create-mouse-sensor.js b/src/view/drag-handle/sensor/create-mouse-sensor.js index db21fa2ce1..7e5cdb724f 100644 --- a/src/view/drag-handle/sensor/create-mouse-sensor.js +++ b/src/view/drag-handle/sensor/create-mouse-sensor.js @@ -116,7 +116,10 @@ export default ({ return; } - startDragging(() => callbacks.onLift({ client: point, isScrollAllowed: true })); + startDragging(() => callbacks.onLift({ + client: point, + autoScrollMode: 'FLUID', + })); }, mouseup: () => { if (state.pending) { diff --git a/src/view/drag-handle/sensor/create-touch-sensor.js b/src/view/drag-handle/sensor/create-touch-sensor.js index a50e3fdbd9..1707bee57f 100644 --- a/src/view/drag-handle/sensor/create-touch-sensor.js +++ b/src/view/drag-handle/sensor/create-touch-sensor.js @@ -71,8 +71,7 @@ export default ({ callbacks.onLift({ client: pending, - // not allowing container scrolling for touch movements at this stage - isScrollAllowed: false, + autoScrollMode: 'FLUID', }); }; const stopDragging = (fn?: Function = noop) => { diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 5035e9e190..b75bbb7914 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -8,6 +8,7 @@ import type { DraggableDimension, InitialDragPositions, DroppableId, + AutoScrollMode, } from '../../types'; import DraggableDimensionPublisher from '../draggable-dimension-publisher/'; import Moveable from '../moveable/'; @@ -102,9 +103,9 @@ export default class Draggable extends Component { this.props.dropAnimationFinished(); } - onLift = (options: {client: Position, isScrollAllowed: boolean}) => { + onLift = (options: {client: Position, autoScrollMode: AutoScrollMode}) => { this.throwIfCannotDrag(); - const { client, isScrollAllowed } = options; + const { client, autoScrollMode } = options; const { lift, draggableId } = this.props; const { ref } = this.state; @@ -119,7 +120,7 @@ export default class Draggable extends Component { const windowScroll: Position = getWindowScrollPosition(); - lift(draggableId, initial, windowScroll, isScrollAllowed); + lift(draggableId, initial, windowScroll, autoScrollMode); } onMove = (client: Position) => { diff --git a/test/unit/state/visibility/is-partially-visible.spec.js b/test/unit/state/visibility/is-partially-visible.spec.js index e69e6b46b4..6aa5a28e40 100644 --- a/test/unit/state/visibility/is-partially-visible.spec.js +++ b/test/unit/state/visibility/is-partially-visible.spec.js @@ -1,6 +1,6 @@ // @flow import getArea from '../../../../src/state/get-area'; -import isPartiallyVisible from '../../../../src/state/visibility/is-partially-visible'; +import { isPartiallyVisible } from '../../../../src/state/visibility/is-visible'; import { getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; import { offset } from '../../../../src/state/spacing'; import type { From 3003c72dfc80eb98767f958a8fba8182071541a4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 29 Jan 2018 09:08:25 +1100 Subject: [PATCH 020/163] more is too big logic --- .../auto-scroll-marshal.js | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index f8263fb28e..5f4174bab9 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -179,6 +179,10 @@ export default ({ return; } + if (isTooBigForAutoScrolling(droppable.viewport.frame, draggable.page.withMargin)) { + return; + } + const requiredFrameScroll: ?Position = getRequiredScroll(droppable.viewport.frame, center); @@ -193,6 +197,7 @@ export default ({ const drag: DragState = (state.drag: any); const offset: Position = (drag.scrollJumpRequest : any); + const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; const destination: ?DraggableLocation = drag.impact.destination; if (!destination) { @@ -200,10 +205,9 @@ export default ({ return; } - const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; - const viewport: Area = getViewport(); + // 1. Can we scroll the viewport? - // draggable is too big + const viewport: Area = getViewport(); if (isTooBigForAutoScrolling(viewport, draggable.page.withMargin)) { return; } @@ -212,14 +216,21 @@ export default ({ scrollWindow(offset); return; } - console.log('cannot scroll window!', offset); + + // 2. Can we scroll the droppable? const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; - // scroll the droppable if it is a scroll container - if (droppable.viewport.frame) { - scrollDroppable(destination.droppableId, offset); + // Current droppable has no frame + if (!droppable.viewport.frame) { + return; } + + if (isTooBigForAutoScrolling(droppable.viewport.frame, draggable.page.withMargin)) { + return; + } + + scrollDroppable(destination.droppableId, offset); }; const onStateChange = (previous: State, current: State): void => { From 6a97c5b88c77cf92cb7416a801b841ab220fecf7 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 29 Jan 2018 10:46:41 +1100 Subject: [PATCH 021/163] correcting keyboard window scrolling --- .../auto-scroll-marshal/auto-scroll-marshal.js | 2 ++ src/state/move-to-next-index/in-home-list.js | 5 +---- .../is-totally-visible-in-new-location.js | 13 +++++++------ .../drag-handle/sensor/create-keyboard-sensor.js | 3 ++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 5f4174bab9..f4986a65ec 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -213,6 +213,7 @@ export default ({ } if (canScrollWindow(offset)) { + // not scheduling - jump requests need to be performed instantly scrollWindow(offset); return; } @@ -230,6 +231,7 @@ export default ({ return; } + // not scheduling - jump requests need to be performed instantly scrollDroppable(destination.droppableId, offset); }; diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 2ba4c8bdbe..d4f464cc26 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -90,19 +90,16 @@ export default ({ }); if (!isVisible) { - // HACK! just experimenting - // const diff: Position = // need to get diff from where we are right now const diff: Position = subtract(newCenter, previousPageCenter); + // request a scroll jump to that position const result: Result = { pageCenter: previousPageCenter, impact: previousImpact, scrollJumpRequest: diff, }; - // console.log('diff', diff); - // window.scrollBy(diff.x, diff.y); return result; } diff --git a/src/state/move-to-next-index/is-totally-visible-in-new-location.js b/src/state/move-to-next-index/is-totally-visible-in-new-location.js index 9fd1654b43..b7dae67dca 100644 --- a/src/state/move-to-next-index/is-totally-visible-in-new-location.js +++ b/src/state/move-to-next-index/is-totally-visible-in-new-location.js @@ -1,7 +1,7 @@ // @flow import { subtract } from '../position'; import { offset } from '../spacing'; -import { isTotallyVisible } from '../visibility/is-visible'; +import { isTotallyVisible, isPartiallyVisible } from '../visibility/is-visible'; import isTotallyVisibleThroughFrame from '../visibility/is-totally-visible-through-frame'; import type { Area, @@ -24,9 +24,12 @@ export default ({ newCenter, viewport, }: Args): boolean => { - // what the new draggable boundary be if it had the new center - const diff: Position = subtract(newCenter, draggable.page.withMargin.center); - const shifted: Spacing = offset(draggable.page.withMargin, diff); + // What would the location of the Draggable be once the move is completed? + // We are not considering margins for this calculation. + // This is because a move might move a Draggable slightly outside of the bounds + // of a Droppable (which is okay) + const diff: Position = subtract(newCenter, draggable.page.withoutMargin.center); + const shifted: Spacing = offset(draggable.page.withoutMargin, diff); // Must be totally visible, not just partially visible. @@ -36,7 +39,5 @@ export default ({ viewport, }); - console.log('is totally visible?', isVisible); - return isVisible; }; diff --git a/src/view/drag-handle/sensor/create-keyboard-sensor.js b/src/view/drag-handle/sensor/create-keyboard-sensor.js index 1cffbd7c53..29ce9f95dd 100644 --- a/src/view/drag-handle/sensor/create-keyboard-sensor.js +++ b/src/view/drag-handle/sensor/create-keyboard-sensor.js @@ -161,8 +161,9 @@ export default ({ }; const windowBindings = { - // any mouse down kills a drag + // any mouse actions kills a drag mousedown: cancel, + mouseup: cancel, resize: cancel, // Cancel if the user is using the mouse wheel // We are not supporting wheel / trackpad scrolling with keyboard dragging From b9946f954537534b5b35834cb6c34c17d47476db Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 29 Jan 2018 12:57:43 +1100 Subject: [PATCH 022/163] progress --- .../auto-scroll-marshal.js | 2 + .../auto-scroll-marshal/scroll-window.js | 1 + src/state/create-store.js | 2 +- src/state/move-to-next-index/in-home-list.js | 61 +++++++++++-------- .../is-totally-visible-in-new-location.js | 8 ++- .../droppable-dimension-publisher.jsx | 2 + stories/src/horizontal/author-app.jsx | 17 +----- stories/src/multiple-horizontal/quote-app.jsx | 18 +----- stories/src/multiple-vertical/quote-app.jsx | 18 +----- stories/src/primatives/author-item.jsx | 1 - stories/src/primatives/title.jsx | 1 - stories/src/vertical-grouped/quote-app.jsx | 18 +----- stories/src/vertical-nested/quote-app.jsx | 16 ----- stories/src/vertical-nested/quote-list.jsx | 3 - 14 files changed, 50 insertions(+), 118 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index f4986a65ec..b9993dfa36 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -231,6 +231,8 @@ export default ({ return; } + console.log('scrolling droppable by', offset); + // not scheduling - jump requests need to be performed instantly scrollDroppable(destination.droppableId, offset); }; diff --git a/src/state/auto-scroll-marshal/scroll-window.js b/src/state/auto-scroll-marshal/scroll-window.js index 1e6c7f97cb..1f52339604 100644 --- a/src/state/auto-scroll-marshal/scroll-window.js +++ b/src/state/auto-scroll-marshal/scroll-window.js @@ -60,6 +60,7 @@ export const canScroll = (change: Position): boolean => { // Not guarenteed to scroll by the entire amount export default (change: Position): void => { if (canScroll(change)) { + console.log('scrolling window by ', change); window.scrollBy(change.x, change.y); } else { console.log('cannot scroll window!', change); diff --git a/src/state/create-store.js b/src/state/create-store.js index 0fb84d4399..459d6dd1ae 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -18,7 +18,7 @@ export default (): Store => createStore( applyMiddleware( thunk, // debugging logger - // require('./debug-middleware/log-middleware').default, + require('./debug-middleware/log-middleware').default, // debugging timer // require('./debug-middleware/timing-middleware').default, ), diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index d4f464cc26..94697e6f20 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -72,7 +72,7 @@ export default ({ return isMovingForward ? 'start' : 'end'; })(); - const newCenter: Position = moveToEdge({ + const newPageCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, sourceEdge: edge, destination: destination.page.withoutMargin, @@ -82,26 +82,8 @@ export default ({ const viewport: Area = getViewport(); - const isVisible: boolean = isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newCenter, - viewport, - }); - - if (!isVisible) { - // need to get diff from where we are right now - const diff: Position = subtract(newCenter, previousPageCenter); - - // request a scroll jump to that position - const result: Result = { - pageCenter: previousPageCenter, - impact: previousImpact, - scrollJumpRequest: diff, - }; - - return result; - } + console.log('old center', previousPageCenter); + console.log('new center', newPageCenter); // Calculate DragImpact // at this point we know that the destination is droppable @@ -146,11 +128,38 @@ export default ({ direction: droppable.axis.direction, }; - const result: Result = { - pageCenter: newCenter, + const isVisible: boolean = isTotallyVisibleInNewLocation({ + draggable, + destination: droppable, + newPageCenter, + viewport, + }); + + if (isVisible) { + const scrollDiff: Position = droppable.viewport.frameScroll.diff.value; + const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); + + return { + pageCenter: withScrollDiff, + impact: newImpact, + scrollJumpRequest: null, + }; + } + + // The full distance required to get from the previous page center to the new page center + const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); + + // We need to consider how much the droppable scroll has changed + const scrollDiff: Position = droppable.viewport.frameScroll.diff.value; + + // The actual scroll required to move into the next place + const requiredScroll: Position = subtract(requiredDistance, scrollDiff); + + return { + // using the previous page center with a new impact + // the subsequent droppable scroll + pageCenter: newPageCenter, impact: newImpact, - scrollJumpRequest: null, + scrollJumpRequest: requiredScroll, }; - - return result; }; diff --git a/src/state/move-to-next-index/is-totally-visible-in-new-location.js b/src/state/move-to-next-index/is-totally-visible-in-new-location.js index b7dae67dca..6373383750 100644 --- a/src/state/move-to-next-index/is-totally-visible-in-new-location.js +++ b/src/state/move-to-next-index/is-totally-visible-in-new-location.js @@ -14,21 +14,21 @@ import type { type Args = {| draggable: DraggableDimension, destination: DroppableDimension, - newCenter: Position, + newPageCenter: Position, viewport: Area, |} export default ({ draggable, destination, - newCenter, + newPageCenter, viewport, }: Args): boolean => { // What would the location of the Draggable be once the move is completed? // We are not considering margins for this calculation. // This is because a move might move a Draggable slightly outside of the bounds // of a Droppable (which is okay) - const diff: Position = subtract(newCenter, draggable.page.withoutMargin.center); + const diff: Position = subtract(newPageCenter, draggable.page.withoutMargin.center); const shifted: Spacing = offset(draggable.page.withoutMargin, diff); // Must be totally visible, not just partially visible. @@ -39,5 +39,7 @@ export default ({ viewport, }); + console.log('is totally visible?', isVisible); + return isVisible; }; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 45c3bb2094..354629dfcd 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -96,6 +96,8 @@ export default class DroppableDimensionPublisher extends Component { return; } + console.log('actually scrolling droppable', change); + this.closestScrollable.scrollTop += change.y; this.closestScrollable.scrollLeft += change.x; } diff --git a/stories/src/horizontal/author-app.jsx b/stories/src/horizontal/author-app.jsx index 6f33818573..44972de260 100644 --- a/stories/src/horizontal/author-app.jsx +++ b/stories/src/horizontal/author-app.jsx @@ -1,6 +1,6 @@ // @flow import React, { Component } from 'react'; -import styled, { injectGlobal } from 'styled-components'; +import styled from 'styled-components'; import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import type { @@ -12,7 +12,6 @@ import AuthorList from '../primatives/author-list'; import reorder from '../reorder'; import { colors, grid } from '../constants'; -const isDraggingClassName = 'is-dragging'; const publishOnDragStart = action('onDragStart'); const publishOnDragEnd = action('onDragEnd'); @@ -38,26 +37,12 @@ export default class AuthorApp extends Component { } /* eslint-enable react/sort-comp */ - componentDidMount() { - // eslint-disable-next-line no-unused-expressions - injectGlobal` - body.${isDraggingClassName} { - cursor: grabbing; - user-select: none; - } - `; - } - onDragStart = (initial: DragStart) => { publishOnDragStart(initial); - // $ExpectError - body wont be null - document.body.classList.add(isDraggingClassName); } onDragEnd = (result: DropResult) => { publishOnDragEnd(result); - // $ExpectError - body wont be null - document.body.classList.remove(isDraggingClassName); // dropped outside the list if (!result.destination) { diff --git a/stories/src/multiple-horizontal/quote-app.jsx b/stories/src/multiple-horizontal/quote-app.jsx index 266d883999..55a90b004d 100644 --- a/stories/src/multiple-horizontal/quote-app.jsx +++ b/stories/src/multiple-horizontal/quote-app.jsx @@ -1,6 +1,6 @@ // @flow import React, { Component } from 'react'; -import styled, { injectGlobal } from 'styled-components'; +import styled from 'styled-components'; import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import AuthorList from '../primatives/author-list'; @@ -24,8 +24,6 @@ const Root = styled.div` flex-direction: column; `; -const isDraggingClassName = 'is-dragging'; - type Props = {| initial: QuoteMap, |} @@ -42,14 +40,10 @@ export default class QuoteApp extends Component { onDragStart = (initial: DragStart) => { publishOnDragStart(initial); - // $ExpectError - body could be null? - document.body.classList.add(isDraggingClassName); } onDragEnd = (result: DropResult) => { publishOnDragEnd(result); - // $ExpectError - body could be null? - document.body.classList.remove(isDraggingClassName); // // dropped outside the list if (!result.destination) { @@ -63,16 +57,6 @@ export default class QuoteApp extends Component { })); } - componentDidMount() { - // eslint-disable-next-line no-unused-expressions - injectGlobal` - body.${isDraggingClassName} { - cursor: grabbing; - user-select: none; - } - `; - } - render() { const { quoteMap, autoFocusQuoteId } = this.state; diff --git a/stories/src/multiple-vertical/quote-app.jsx b/stories/src/multiple-vertical/quote-app.jsx index 71bd69460b..c1e8bd6cc8 100644 --- a/stories/src/multiple-vertical/quote-app.jsx +++ b/stories/src/multiple-vertical/quote-app.jsx @@ -1,6 +1,6 @@ // @flow import React, { Component } from 'react'; -import styled, { injectGlobal } from 'styled-components'; +import styled from 'styled-components'; import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import QuoteList from '../primatives/quote-list'; @@ -54,8 +54,6 @@ const PushDown = styled.div` height: 200px; `; -const isDraggingClassName = 'is-dragging'; - type Props = {| initial: QuoteMap, |} @@ -75,14 +73,10 @@ export default class QuoteApp extends Component { // this.setState({ // disabledDroppable: this.getDisabledDroppable(initial.source.droppableId), // }); - // $ExpectError - body could be null? - document.body.classList.add(isDraggingClassName); } onDragEnd = (result: DropResult) => { publishOnDragEnd(result); - // $ExpectError - body could be null? - document.body.classList.remove(isDraggingClassName); // dropped nowhere if (!result.destination) { @@ -99,16 +93,6 @@ export default class QuoteApp extends Component { })); } - componentDidMount() { - // eslint-disable-next-line no-unused-expressions - injectGlobal` - body.${isDraggingClassName} { - cursor: grabbing; - user-select: none; - } - `; - } - // TODO getDisabledDroppable = (sourceDroppable: ?string) => { if (!sourceDroppable) { diff --git a/stories/src/primatives/author-item.jsx b/stories/src/primatives/author-item.jsx index 0ab27ad275..cfba27c41b 100644 --- a/stories/src/primatives/author-item.jsx +++ b/stories/src/primatives/author-item.jsx @@ -12,7 +12,6 @@ const Avatar = styled.img` width: 60px; height: 60px; border-radius: 50%; - cursor: grab; margin-right: ${grid}px; border-color: ${({ isDragging }) => (isDragging ? colors.green : colors.white)}; border-style: solid; diff --git a/stories/src/primatives/title.jsx b/stories/src/primatives/title.jsx index ac683992f4..aeaf20b92a 100644 --- a/stories/src/primatives/title.jsx +++ b/stories/src/primatives/title.jsx @@ -4,7 +4,6 @@ import { colors, grid } from '../constants'; export default styled.h4` padding: ${grid}px; - cursor: grab; transition: background-color ease 0.2s; flex-grow: 1; user-select: none; diff --git a/stories/src/vertical-grouped/quote-app.jsx b/stories/src/vertical-grouped/quote-app.jsx index e3f611ff8d..6c46e0ad39 100644 --- a/stories/src/vertical-grouped/quote-app.jsx +++ b/stories/src/vertical-grouped/quote-app.jsx @@ -1,6 +1,6 @@ // @flow import React, { Component } from 'react'; -import styled, { injectGlobal } from 'styled-components'; +import styled from 'styled-components'; import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import QuoteList from '../primatives/quote-list'; @@ -37,8 +37,6 @@ const Title = styled.h4` margin: ${grid}px; `; -const isDraggingClassName = 'is-dragging'; - type Props = {| initial: QuoteMap, |} @@ -56,14 +54,10 @@ export default class QuoteApp extends Component { onDragStart = (initial: DragStart) => { publishOnDragStart(initial); - // $ExpectError - body could be null? - document.body.classList.add(isDraggingClassName); } onDragEnd = (result: DropResult) => { publishOnDragEnd(result); - // $ExpectError - body could be null? - document.body.classList.remove(isDraggingClassName); if (!result.destination) { return; @@ -78,16 +72,6 @@ export default class QuoteApp extends Component { this.setState({ quoteMap }); } - componentDidMount() { - // eslint-disable-next-line no-unused-expressions - injectGlobal` - body.${isDraggingClassName} { - cursor: grabbing; - user-select: none; - } - `; - } - render() { const { quoteMap } = this.state; diff --git a/stories/src/vertical-nested/quote-app.jsx b/stories/src/vertical-nested/quote-app.jsx index 8994afb12c..962977dfc2 100644 --- a/stories/src/vertical-nested/quote-app.jsx +++ b/stories/src/vertical-nested/quote-app.jsx @@ -42,8 +42,6 @@ const Root = styled.div` align-items: flex-start; `; -const isDraggingClassName = 'is-dragging'; - type State = {| list: NestedQuoteList, |} @@ -55,26 +53,12 @@ export default class QuoteApp extends Component<*, State> { }; /* eslint-enable */ - componentDidMount() { - // eslint-disable-next-line no-unused-expressions - injectGlobal` - body.${isDraggingClassName} { - cursor: grabbing; - user-select: none; - } - `; - } - onDragStart = (initial: DragStart) => { publishOnDragStart(initial); - // $ExpectError - body could be null? - document.body.classList.add(isDraggingClassName); } onDragEnd = (result: DropResult) => { publishOnDragEnd(result); - // $ExpectError - body could be null? - document.body.classList.remove(isDraggingClassName); // dropped outside the list if (!result.destination) { diff --git a/stories/src/vertical-nested/quote-list.jsx b/stories/src/vertical-nested/quote-list.jsx index b5e8cb56c2..78a2cea234 100644 --- a/stories/src/vertical-nested/quote-list.jsx +++ b/stories/src/vertical-nested/quote-list.jsx @@ -35,9 +35,6 @@ const Container = styled.div` const NestedContainer = Container.extend` padding: 0; margin-bottom: ${grid}px; - &:hover { - cursor: grab; - } `; export default class QuoteList extends Component<{ list: NestedQuoteList }> { From a2858bbcdfb5fa71e517fafe1bb82d4cd2815786 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 29 Jan 2018 13:28:10 +1100 Subject: [PATCH 023/163] giving more information for scroll jumping --- .../auto-scroll-marshal.js | 35 +++++++++++-------- .../auto-scroll-marshal/scroll-window.js | 6 ---- src/state/get-displacement.js | 2 +- .../move-cross-axis/get-closest-draggable.js | 2 +- src/state/move-to-next-index/in-home-list.js | 13 +++++-- .../is-totally-visible-in-new-location.js | 12 +++---- .../move-to-next-index-types.js | 3 +- src/state/visibility/is-visible.js | 26 +++++++++++--- src/types.js | 9 +++-- stories/src/vertical-nested/quote-app.jsx | 2 +- 10 files changed, 67 insertions(+), 43 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index b9993dfa36..bc11a7f0d7 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -17,6 +17,7 @@ import type { Spacing, DraggableLocation, DraggableDimension, + ScrollJumpRequest, } from '../../types'; type Args = {| @@ -194,8 +195,17 @@ export default ({ }; const jumpScroll = (state: State) => { - const drag: DragState = (state.drag: any); - const offset: Position = (drag.scrollJumpRequest : any); + const drag: ?DragState = state.drag; + + if (!drag) { + return; + } + + const request: ?ScrollJumpRequest = drag.scrollJumpRequest; + + if (!request) { + return; + } const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; const destination: ?DraggableLocation = drag.impact.destination; @@ -205,25 +215,22 @@ export default ({ return; } - // 1. Can we scroll the viewport? - - const viewport: Area = getViewport(); - if (isTooBigForAutoScrolling(viewport, draggable.page.withMargin)) { - return; - } + if (request.target === 'WINDOW') { + if (isTooBigForAutoScrolling(getViewport(), draggable.page.withMargin)) { + return; + } - if (canScrollWindow(offset)) { - // not scheduling - jump requests need to be performed instantly - scrollWindow(offset); + scrollWindow(request.scroll); return; } - // 2. Can we scroll the droppable? + // trying to scroll a droppable const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; // Current droppable has no frame if (!droppable.viewport.frame) { + console.warn('Cannot scroll droppable as requested as it is not scrollable'); return; } @@ -231,10 +238,8 @@ export default ({ return; } - console.log('scrolling droppable by', offset); - // not scheduling - jump requests need to be performed instantly - scrollDroppable(destination.droppableId, offset); + scrollDroppable(destination.droppableId, request.scroll); }; const onStateChange = (previous: State, current: State): void => { diff --git a/src/state/auto-scroll-marshal/scroll-window.js b/src/state/auto-scroll-marshal/scroll-window.js index 1f52339604..02f6963152 100644 --- a/src/state/auto-scroll-marshal/scroll-window.js +++ b/src/state/auto-scroll-marshal/scroll-window.js @@ -14,12 +14,6 @@ const getSmallestSignedValue = (value: number) => { return value > 0 ? 1 : -1; }; -type Args = {| - draggable: DraggableDimension, - change: Position, - viewport: Area, -|} - // Will return true if can scroll even a little bit in either direction // of the change. export const canScroll = (change: Position): boolean => { diff --git a/src/state/get-displacement.js b/src/state/get-displacement.js index 80d7e16b0e..45b13ed6ee 100644 --- a/src/state/get-displacement.js +++ b/src/state/get-displacement.js @@ -31,7 +31,7 @@ export default ({ target: draggable.page.withMargin, destination, viewport, - }); + }).isVisible; const shouldAnimate: boolean = (() => { // if should be displaced and not visible diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index 00b5da4261..a2822e316c 100644 --- a/src/state/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-cross-axis/get-closest-draggable.js @@ -40,7 +40,7 @@ export default ({ target: draggable.page.withMargin, destination, viewport, - })) + }).isVisible) .sort((a: DraggableDimension, b: DraggableDimension): number => { const distanceToA = distance(pageCenter, a.page.withMargin.center); const distanceToB = distance(pageCenter, b.page.withMargin.center); diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 94697e6f20..f117e6aeb2 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -2,6 +2,7 @@ import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import { patch, subtract } from '../position'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; +import type { IsVisibleResult } from '../visibility/is-visible'; import getViewport from '../visibility/get-viewport'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; @@ -15,6 +16,7 @@ import type { Axis, DragImpact, Area, + ScrollJumpRequest, } from '../../types'; export default ({ @@ -128,14 +130,14 @@ export default ({ direction: droppable.axis.direction, }; - const isVisible: boolean = isTotallyVisibleInNewLocation({ + const result: IsVisibleResult = isTotallyVisibleInNewLocation({ draggable, destination: droppable, newPageCenter, viewport, }); - if (isVisible) { + if (result.isVisible) { const scrollDiff: Position = droppable.viewport.frameScroll.diff.value; const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); @@ -155,11 +157,16 @@ export default ({ // The actual scroll required to move into the next place const requiredScroll: Position = subtract(requiredDistance, scrollDiff); + const request: ScrollJumpRequest = { + scroll: requiredScroll, + target: result.isVisibleInDroppable ? 'WINDOW' : 'DROPPABLE', + }; + return { // using the previous page center with a new impact // the subsequent droppable scroll pageCenter: newPageCenter, impact: newImpact, - scrollJumpRequest: requiredScroll, + scrollJumpRequest: request, }; }; diff --git a/src/state/move-to-next-index/is-totally-visible-in-new-location.js b/src/state/move-to-next-index/is-totally-visible-in-new-location.js index 6373383750..1cffe212ed 100644 --- a/src/state/move-to-next-index/is-totally-visible-in-new-location.js +++ b/src/state/move-to-next-index/is-totally-visible-in-new-location.js @@ -1,8 +1,8 @@ // @flow import { subtract } from '../position'; import { offset } from '../spacing'; -import { isTotallyVisible, isPartiallyVisible } from '../visibility/is-visible'; -import isTotallyVisibleThroughFrame from '../visibility/is-totally-visible-through-frame'; +import { isTotallyVisible } from '../visibility/is-visible'; +import type { IsVisibleResult } from '../visibility/is-visible'; import type { Area, DraggableDimension, @@ -23,7 +23,7 @@ export default ({ destination, newPageCenter, viewport, -}: Args): boolean => { +}: Args): IsVisibleResult => { // What would the location of the Draggable be once the move is completed? // We are not considering margins for this calculation. // This is because a move might move a Draggable slightly outside of the bounds @@ -33,13 +33,9 @@ export default ({ // Must be totally visible, not just partially visible. - const isVisible: boolean = isTotallyVisible({ + return isTotallyVisible({ target: shifted, destination, viewport, }); - - console.log('is totally visible?', isVisible); - - return isVisible; }; diff --git a/src/state/move-to-next-index/move-to-next-index-types.js b/src/state/move-to-next-index/move-to-next-index-types.js index 3c60dccae1..b9f8344460 100644 --- a/src/state/move-to-next-index/move-to-next-index-types.js +++ b/src/state/move-to-next-index/move-to-next-index-types.js @@ -5,6 +5,7 @@ import type { DragImpact, DroppableDimension, DraggableDimensionMap, + ScrollJumpRequest, } from '../../types'; export type Args = {| @@ -24,5 +25,5 @@ export type Result = {| // Any scroll that is required for the movement. // If this is present then the pageCenter and impact // will just be the same as the previous drag - scrollJumpRequest: ?Position, + scrollJumpRequest: ?ScrollJumpRequest, |} diff --git a/src/state/visibility/is-visible.js b/src/state/visibility/is-visible.js index 6b85d29cc4..7037d66454 100644 --- a/src/state/visibility/is-visible.js +++ b/src/state/visibility/is-visible.js @@ -20,19 +20,31 @@ type HelperArgs = {| isVisibleThroughFrameFn: (frame: Spacing) => (subject: Spacing) => boolean |} +export type IsVisibleResult = {| + isVisible: boolean, + isVisibleInViewport: boolean, + isVisibleInDroppable: boolean, +|} + +const nope: IsVisibleResult = { + isVisible: false, + isVisibleInDroppable: false, + isVisibleInViewport: false, +}; + const isVisible = ({ target, destination, viewport, isVisibleThroughFrameFn, -}: HelperArgs): boolean => { +}: HelperArgs): IsVisibleResult => { const displacement: Position = destination.viewport.frameScroll.diff.displacement; const withScroll: Spacing = offset(target, displacement); // destination subject is totally hidden by frame // this should never happen - but just guarding against it if (!destination.viewport.clipped) { - return false; + return nope; } // When considering if the target is visible in the droppable we need @@ -47,14 +59,18 @@ const isVisible = ({ const isVisibleInViewport: boolean = isVisibleThroughFrameFn(viewport)(withScroll); - return isVisibleInDroppable && isVisibleInViewport; + return { + isVisible: isVisibleInDroppable && isVisibleInViewport, + isVisibleInDroppable, + isVisibleInViewport, + }; }; export const isPartiallyVisible = ({ target, destination, viewport, -}: Args): boolean => isVisible({ +}: Args): IsVisibleResult => isVisible({ target, destination, viewport, @@ -65,7 +81,7 @@ export const isTotallyVisible = ({ target, destination, viewport, -}: Args): boolean => isVisible({ +}: Args): IsVisibleResult => isVisible({ target, destination, viewport, diff --git a/src/types.js b/src/types.js index f880f2878e..389c39152f 100644 --- a/src/types.js +++ b/src/types.js @@ -238,12 +238,17 @@ export type DropResult = {| destination: ?DraggableLocation, |} +export type ScrollJumpRequest = {| + scroll: Position, + target: 'WINDOW' | 'DROPPABLE', +|} + export type DragState = {| initial: InitialDrag, current: CurrentDrag, - // if we need to jump the scroll - how much we need to jump - scrollJumpRequest: ?Position, impact: DragImpact, + // if we need to jump the scroll (keyboard dragging) + scrollJumpRequest: ?ScrollJumpRequest, |} export type DropTrigger = 'DROP' | 'CANCEL'; diff --git a/stories/src/vertical-nested/quote-app.jsx b/stories/src/vertical-nested/quote-app.jsx index 962977dfc2..bfed014d27 100644 --- a/stories/src/vertical-nested/quote-app.jsx +++ b/stories/src/vertical-nested/quote-app.jsx @@ -1,6 +1,6 @@ // @flow import React, { Component } from 'react'; -import styled, { injectGlobal } from 'styled-components'; +import styled from 'styled-components'; import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import { colors, grid } from '../constants'; From ac909e9e391bb4a1f34b3338aa7f2cc0de9fed22 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 29 Jan 2018 14:08:59 +1100 Subject: [PATCH 024/163] more fiddling --- src/state/move-to-next-index/get-result.js | 59 ++++++++++++++ .../move-to-next-index/in-foreign-list.js | 81 +++++++++++++------ src/state/move-to-next-index/in-home-list.js | 43 +++------- 3 files changed, 123 insertions(+), 60 deletions(-) create mode 100644 src/state/move-to-next-index/get-result.js diff --git a/src/state/move-to-next-index/get-result.js b/src/state/move-to-next-index/get-result.js new file mode 100644 index 0000000000..1bf4946b09 --- /dev/null +++ b/src/state/move-to-next-index/get-result.js @@ -0,0 +1,59 @@ +// @flow +import { subtract } from '../position'; +import type { Result } from './move-to-next-index-types'; +import type { IsVisibleResult } from '../visibility/is-visible'; +import type { + DroppableDimension, + Position, + DragImpact, + ScrollJumpRequest, +} from '../../types'; + +type Args = {| + destination: DroppableDimension, + previousPageCenter: Position, + newPageCenter: Position, + newImpact: DragImpact, + isVisibleResult: IsVisibleResult, +|} + +export default ({ + destination, + previousPageCenter, + newPageCenter, + newImpact, + isVisibleResult, +}: Args): Result => { + if (isVisibleResult.isVisible) { + const scrollDiff: Position = destination.viewport.frameScroll.diff.value; + const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); + + return { + pageCenter: withScrollDiff, + impact: newImpact, + scrollJumpRequest: null, + }; + } + + // The full distance required to get from the previous page center to the new page center + const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); + + // We need to consider how much the droppable scroll has changed + const scrollDiff: Position = destination.viewport.frameScroll.diff.value; + + // The actual scroll required to move into the next place + const requiredScroll: Position = subtract(requiredDistance, scrollDiff); + + const request: ScrollJumpRequest = { + scroll: requiredScroll, + target: isVisibleResult.isVisibleInDroppable ? 'WINDOW' : 'DROPPABLE', + }; + + return { + // using the previous page center with a new impact + // the subsequent droppable scroll + pageCenter: newPageCenter, + impact: newImpact, + scrollJumpRequest: request, + }; +}; diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index ee2ffc7230..9c4f7ec292 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -5,8 +5,10 @@ import moveToEdge from '../move-to-edge'; import getDisplacement from '../get-displacement'; import getViewport from '../visibility/get-viewport'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; +import getResult from './get-result'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; +import type { IsVisibleResult } from '../visibility/is-visible'; import type { DraggableLocation, DraggableDimension, @@ -21,6 +23,7 @@ export default ({ isMovingForward, draggableId, previousImpact, + previousPageCenter, droppable, draggables, }: Args): ?Result => { @@ -73,7 +76,7 @@ export default ({ })(); const viewport: Area = getViewport(); - const newCenter: Position = moveToEdge({ + const newPageCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, sourceEdge, destination: movingRelativeTo.page.withMargin, @@ -81,27 +84,27 @@ export default ({ destinationAxis: droppable.axis, }); - const isVisible: boolean = (() => { - // Moving into placeholder position - // Usually this would be outside of the visible bounds - if (isMovingPastLastIndex) { - return true; - } - - // checking the shifted draggable rather than just the new center - // as the new center might not be visible but the whole draggable - // might be partially visible - return isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newCenter, - viewport, - }); - })(); - - if (!isVisible) { - return null; - } + // const isVisible: boolean = (() => { + // // Moving into placeholder position + // // Usually this would be outside of the visible bounds + // if (isMovingPastLastIndex) { + // return true; + // } + + // // checking the shifted draggable rather than just the new center + // // as the new center might not be visible but the whole draggable + // // might be partially visible + // return isTotallyVisibleInNewLocation({ + // draggable, + // destination: droppable, + // newCenter, + // viewport, + // }); + // })(); + + // if (!isVisible) { + // return null; + // } // at this point we know that the destination is droppable const movingRelativeToDisplacement: Displacement = { @@ -153,8 +156,34 @@ export default ({ direction: droppable.axis.direction, }; - return { - pageCenter: newCenter, - impact: newImpact, - }; + const result: IsVisibleResult = (() => { + // Moving into placeholder position + // Usually this would be outside of the visible bounds + if (isMovingPastLastIndex) { + return { + isVisible: true, + isVisibleInViewport: true, + isVisibleInDroppable: true, + }; + } + + // checking the shifted draggable rather than just the new center + // as the new center might not be visible but the whole draggable + // might be partially visible + return isTotallyVisibleInNewLocation({ + draggable, + destination: droppable, + newPageCenter, + viewport, + }); + })(); + + // not visible + return getResult({ + destination: droppable, + previousPageCenter, + newPageCenter, + newImpact, + isVisibleResult: result, + }); }; diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index f117e6aeb2..d3154177b7 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,6 +1,6 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch, subtract } from '../position'; +import { patch } from '../position'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import type { IsVisibleResult } from '../visibility/is-visible'; import getViewport from '../visibility/get-viewport'; @@ -8,6 +8,7 @@ import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import getDisplacement from '../get-displacement'; +import getResult from './get-result'; import type { DraggableLocation, DraggableDimension, @@ -16,7 +17,6 @@ import type { Axis, DragImpact, Area, - ScrollJumpRequest, } from '../../types'; export default ({ @@ -137,36 +137,11 @@ export default ({ viewport, }); - if (result.isVisible) { - const scrollDiff: Position = droppable.viewport.frameScroll.diff.value; - const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); - - return { - pageCenter: withScrollDiff, - impact: newImpact, - scrollJumpRequest: null, - }; - } - - // The full distance required to get from the previous page center to the new page center - const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); - - // We need to consider how much the droppable scroll has changed - const scrollDiff: Position = droppable.viewport.frameScroll.diff.value; - - // The actual scroll required to move into the next place - const requiredScroll: Position = subtract(requiredDistance, scrollDiff); - - const request: ScrollJumpRequest = { - scroll: requiredScroll, - target: result.isVisibleInDroppable ? 'WINDOW' : 'DROPPABLE', - }; - - return { - // using the previous page center with a new impact - // the subsequent droppable scroll - pageCenter: newPageCenter, - impact: newImpact, - scrollJumpRequest: request, - }; + return getResult({ + destination: droppable, + previousPageCenter, + newPageCenter, + newImpact, + isVisibleResult: result, + }); }; From db45ca64b42aac008161b469de16feab01cae109 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 29 Jan 2018 16:18:37 +1100 Subject: [PATCH 025/163] in home list seems to be working --- src/state/create-store.js | 2 +- src/state/move-to-next-index/get-result.js | 24 +++++--- .../move-to-next-index/in-foreign-list.js | 27 ++------- src/state/move-to-next-index/in-home-list.js | 59 +++++++++++++------ src/state/reducer.js | 33 ++++++----- stories/src/data.js | 2 +- 6 files changed, 82 insertions(+), 65 deletions(-) diff --git a/src/state/create-store.js b/src/state/create-store.js index 459d6dd1ae..0fb84d4399 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -18,7 +18,7 @@ export default (): Store => createStore( applyMiddleware( thunk, // debugging logger - require('./debug-middleware/log-middleware').default, + // require('./debug-middleware/log-middleware').default, // debugging timer // require('./debug-middleware/timing-middleware').default, ), diff --git a/src/state/move-to-next-index/get-result.js b/src/state/move-to-next-index/get-result.js index 1bf4946b09..8495b0359d 100644 --- a/src/state/move-to-next-index/get-result.js +++ b/src/state/move-to-next-index/get-result.js @@ -1,9 +1,12 @@ // @flow import { subtract } from '../position'; +import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; +import getViewport from '../visibility/get-viewport'; import type { Result } from './move-to-next-index-types'; import type { IsVisibleResult } from '../visibility/is-visible'; import type { DroppableDimension, + DraggableDimension, Position, DragImpact, ScrollJumpRequest, @@ -11,20 +14,27 @@ import type { type Args = {| destination: DroppableDimension, + draggable: DraggableDimension, previousPageCenter: Position, newPageCenter: Position, newImpact: DragImpact, - isVisibleResult: IsVisibleResult, |} export default ({ destination, + draggable, previousPageCenter, newPageCenter, newImpact, - isVisibleResult, }: Args): Result => { - if (isVisibleResult.isVisible) { + const isTotallyVisible: IsVisibleResult = isTotallyVisibleInNewLocation({ + draggable, + destination, + newPageCenter, + viewport: getViewport(), + }); + + if (isTotallyVisible.isVisible) { const scrollDiff: Position = destination.viewport.frameScroll.diff.value; const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); @@ -46,13 +56,13 @@ export default ({ const request: ScrollJumpRequest = { scroll: requiredScroll, - target: isVisibleResult.isVisibleInDroppable ? 'WINDOW' : 'DROPPABLE', + target: isTotallyVisible.isVisibleInDroppable ? 'WINDOW' : 'DROPPABLE', }; return { - // using the previous page center with a new impact - // the subsequent droppable scroll - pageCenter: newPageCenter, + // Using the previous page center with a new impact + // as we are not visually moving the Draggable + pageCenter: previousPageCenter, impact: newImpact, scrollJumpRequest: request, }; diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index 9c4f7ec292..ed6320a245 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -156,34 +156,15 @@ export default ({ direction: droppable.axis.direction, }; - const result: IsVisibleResult = (() => { - // Moving into placeholder position - // Usually this would be outside of the visible bounds - if (isMovingPastLastIndex) { - return { - isVisible: true, - isVisibleInViewport: true, - isVisibleInDroppable: true, - }; - } - - // checking the shifted draggable rather than just the new center - // as the new center might not be visible but the whole draggable - // might be partially visible - return isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newPageCenter, - viewport, - }); - })(); + if (isMovingPastLastIndex) { + // TODO! + } - // not visible return getResult({ + draggable, destination: droppable, previousPageCenter, newPageCenter, newImpact, - isVisibleResult: result, }); }; diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index d3154177b7..83c8d208d7 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,6 +1,6 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch } from '../position'; +import { subtract, patch } from '../position'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import type { IsVisibleResult } from '../visibility/is-visible'; import getViewport from '../visibility/get-viewport'; @@ -8,7 +8,7 @@ import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import getDisplacement from '../get-displacement'; -import getResult from './get-result'; +import { isTotallyVisible } from '../visibility/is-visible'; import type { DraggableLocation, DraggableDimension, @@ -17,6 +17,7 @@ import type { Axis, DragImpact, Area, + ScrollJumpRequest, } from '../../types'; export default ({ @@ -82,10 +83,36 @@ export default ({ destinationAxis: droppable.axis, }); - const viewport: Area = getViewport(); + const willBeVisible: IsVisibleResult = isTotallyVisibleInNewLocation({ + draggable, + destination: droppable, + newPageCenter, + viewport: getViewport(), + }); - console.log('old center', previousPageCenter); - console.log('new center', newPageCenter); + if (!willBeVisible.isVisible) { + // The full distance required to get from the previous page center to the new page center + const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); + + // We need to consider how much the droppable scroll has changed + const scrollDiff: Position = droppable.viewport.frameScroll.diff.value; + + // The actual scroll required to move into the next place + const requiredScroll: Position = subtract(requiredDistance, scrollDiff); + + const request: ScrollJumpRequest = { + scroll: requiredScroll, + target: isTotallyVisible.isVisibleInDroppable ? 'WINDOW' : 'DROPPABLE', + }; + + return { + // Using the previous page center with a new impact + // as we are not visually moving the Draggable + pageCenter: previousPageCenter, + impact: previousImpact, + scrollJumpRequest: request, + }; + } // Calculate DragImpact // at this point we know that the destination is droppable @@ -102,10 +129,12 @@ export default ({ [destinationDisplacement, ...previousImpact.movement.displaced]); // update impact with visibility - stops redundant work! + const viewport: Area = getViewport(); const displaced: Displacement[] = modified .map((displacement: Displacement): Displacement => { const target: DraggableDimension = draggables[displacement.draggableId]; + // TODO: the visibility post drag might be different to this! const updated: Displacement = getDisplacement({ draggable: target, destination: droppable, @@ -130,18 +159,12 @@ export default ({ direction: droppable.axis.direction, }; - const result: IsVisibleResult = isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newPageCenter, - viewport, - }); + const scrollDiff: Position = droppable.viewport.frameScroll.diff.value; + const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); - return getResult({ - destination: droppable, - previousPageCenter, - newPageCenter, - newImpact, - isVisibleResult: result, - }); + return { + pageCenter: withScrollDiff, + impact: newImpact, + scrollJumpRequest: null, + }; }; diff --git a/src/state/reducer.js b/src/state/reducer.js index b3bc083c70..7195d329e4 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -22,6 +22,7 @@ import type { CurrentDragPositions, Position, InitialDragPositions, + ScrollJumpRequest, } from '../types'; import { add, subtract, isEqual } from './position'; import { noMovement } from './no-impact'; @@ -52,8 +53,10 @@ type MoveArgs = {| clientSelection: Position, shouldAnimate: boolean, windowScroll ?: Position, - // force a custom drag impact + // force a custom drag impact (optionally provided) impact?: DragImpact, + // provide a scroll jump request (optionally provided - and can be null) + scrollJumpRequest?: ?ScrollJumpRequest, |} const canPublishDimension = (phase: Phase): boolean => @@ -66,6 +69,7 @@ const move = ({ shouldAnimate, windowScroll, impact, + scrollJumpRequest, }: MoveArgs): State => { if (state.phase !== 'DRAGGING') { console.error('cannot move while not dragging'); @@ -119,7 +123,7 @@ const move = ({ initial, impact: newImpact, current, - scrollJumpRequest: null, + scrollJumpRequest, }; return { @@ -145,6 +149,14 @@ const updateStateAfterDimensionChange = (newState: State): State => { return clean(); } + // If in JUMP auto scroll mode - then impacts are calculated before the scroll + // actually occurs + // const usePreviousImpact: boolean = newState.drag.initial.autoScrollMode === 'JUMP'; + + // if (usePreviousImpact) { + // console.log('USING PREVIOUS IMPACT'); + // } + return move({ state: newState, // use the existing values @@ -320,7 +332,9 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } - if (state.drag == null) { + const drag: ?DragState = state.drag; + + if (drag == null) { console.error('invalid store state'); return clean(); } @@ -457,18 +471,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { return state; } - // requesting a scroll jump - if (result.scrollJumpRequest) { - return { - ...state, - phase: 'DRAGGING', - drag: { - ...existing, - scrollJumpRequest: result.scrollJumpRequest, - }, - }; - } - const impact: DragImpact = result.impact; const page: Position = result.pageCenter; const client: Position = subtract(page, existing.current.windowScroll); @@ -478,6 +480,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { impact, clientSelection: client, shouldAnimate: true, + scrollJumpRequest: result.scrollJumpRequest, }); } diff --git a/stories/src/data.js b/stories/src/data.js index a973ce3cf8..db92966973 100644 --- a/stories/src/data.js +++ b/stories/src/data.js @@ -66,7 +66,7 @@ export const quotes: Quote[] = [ }, { id: '7', - content: 'That\'s it! The answer was so simple, I was too smart to see it!', + content: 'That\'s it! The answer was so simple, I was too smart to see it!, That\'s it! The answer was so simple, I was too smart to see it!, That\'s it! The answer was so simple, I was too smart to see it!, That\'s it! The answer was so simple, I was too smart to see it!', author: princess, }, { From f538cedb0412620946e3256eb7820fae91226ef4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 29 Jan 2018 16:42:54 +1100 Subject: [PATCH 026/163] in home list is looking niace --- .../auto-scroll-marshal/auto-scroll-marshal.js | 14 ++++++++------ src/state/move-to-next-index/in-home-list.js | 9 ++++++--- src/types.js | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index bc11a7f0d7..76c01dd446 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -215,16 +215,18 @@ export default ({ return; } - if (request.target === 'WINDOW') { - if (isTooBigForAutoScrolling(getViewport(), draggable.page.withMargin)) { + if (isTooBigForAutoScrolling(getViewport(), draggable.page.withMargin)) { + return; + } + + if (request.toBeScrolled === 'ANY') { + if (canScrollWindow(request.scroll)) { + scrollWindow(request.scroll); return; } - - scrollWindow(request.scroll); - return; } - // trying to scroll a droppable + // trying to scroll ANY or DROPPABLE const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 83c8d208d7..a66a804ac0 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -83,14 +83,14 @@ export default ({ destinationAxis: droppable.axis, }); - const willBeVisible: IsVisibleResult = isTotallyVisibleInNewLocation({ + const newLocationVisibility: IsVisibleResult = isTotallyVisibleInNewLocation({ draggable, destination: droppable, newPageCenter, viewport: getViewport(), }); - if (!willBeVisible.isVisible) { + if (!newLocationVisibility.isVisible) { // The full distance required to get from the previous page center to the new page center const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); @@ -100,9 +100,12 @@ export default ({ // The actual scroll required to move into the next place const requiredScroll: Position = subtract(requiredDistance, scrollDiff); + // need to prioritise scrolling a droppable so that we do not leave its boundaries + const toBeScrolled = newLocationVisibility.isVisibleInViewport ? 'DROPPABLE' : 'ANY'; + const request: ScrollJumpRequest = { scroll: requiredScroll, - target: isTotallyVisible.isVisibleInDroppable ? 'WINDOW' : 'DROPPABLE', + toBeScrolled, }; return { diff --git a/src/types.js b/src/types.js index 389c39152f..931720e786 100644 --- a/src/types.js +++ b/src/types.js @@ -240,7 +240,7 @@ export type DropResult = {| export type ScrollJumpRequest = {| scroll: Position, - target: 'WINDOW' | 'DROPPABLE', + toBeScrolled: 'ANY' | 'DROPPABLE', |} export type DragState = {| From a1e8ab11e50fcbc8532e9d2e6529dc07e514ee22 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 30 Jan 2018 08:00:48 +1100 Subject: [PATCH 027/163] correcting droppable scroll --- src/state/move-cross-axis/get-closest-draggable.js | 7 ++++--- src/state/move-to-next-index/in-home-list.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index a2822e316c..093a224e76 100644 --- a/src/state/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-cross-axis/get-closest-draggable.js @@ -1,7 +1,7 @@ // @flow import { distance } from '../position'; import getViewport from '../visibility/get-viewport'; -import { isPartiallyVisible } from '../visibility/is-visible'; +import { isTotallyVisible } from '../visibility/is-visible'; import type { Area, Axis, @@ -34,14 +34,15 @@ export default ({ const result: DraggableDimension[] = insideDestination // Remove any options that are hidden by overflow - // Draggable must be partially visible to move to it + // Draggable must be totally visible to move to it .filter((draggable: DraggableDimension): boolean => - isPartiallyVisible({ + isTotallyVisible({ target: draggable.page.withMargin, destination, viewport, }).isVisible) .sort((a: DraggableDimension, b: DraggableDimension): number => { + // TODO: need to consider droppable scroll const distanceToA = distance(pageCenter, a.page.withMargin.center); const distanceToB = distance(pageCenter, b.page.withMargin.center); diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index a66a804ac0..482322c8b0 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -101,7 +101,7 @@ export default ({ const requiredScroll: Position = subtract(requiredDistance, scrollDiff); // need to prioritise scrolling a droppable so that we do not leave its boundaries - const toBeScrolled = newLocationVisibility.isVisibleInViewport ? 'DROPPABLE' : 'ANY'; + const toBeScrolled = newLocationVisibility.isVisibleInDroppable ? 'ANY' : 'DROPPABLE'; const request: ScrollJumpRequest = { scroll: requiredScroll, From 2e0f1f1c5ea7bf76ad72294a44f105e7d66c639b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 30 Jan 2018 09:35:04 +1100 Subject: [PATCH 028/163] new droppable dimension type --- .../auto-scroll-marshal.js | 26 +-- src/state/dimension.js | 189 ++++++++++++------ src/state/spacing.js | 32 +-- src/types.js | 28 ++- .../droppable-dimension-publisher.jsx | 30 ++- 5 files changed, 191 insertions(+), 114 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 76c01dd446..63a9cf7e5c 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -4,6 +4,7 @@ import getViewport from '../visibility/get-viewport'; import { isEqual } from '../position'; import { vertical, horizontal } from '../axis'; import getDroppableFrameOver from './get-droppable-frame-over'; +import { canScrollDroppable } from '../dimension'; import scrollWindow, { canScroll as canScrollWindow } from './scroll-window'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; import type { @@ -215,33 +216,28 @@ export default ({ return; } - if (isTooBigForAutoScrolling(getViewport(), draggable.page.withMargin)) { + const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; + + if (droppable.viewport.clipped == null) { return; } - if (request.toBeScrolled === 'ANY') { - if (canScrollWindow(request.scroll)) { - scrollWindow(request.scroll); - return; - } + if (isTooBigForAutoScrolling(getViewport(), draggable.page.withMargin)) { + return; } - // trying to scroll ANY or DROPPABLE - - const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; - - // Current droppable has no frame - if (!droppable.viewport.frame) { - console.warn('Cannot scroll droppable as requested as it is not scrollable'); + if (isTooBigForAutoScrolling(droppable.viewport.clipped, draggable.page.withMargin)) { return; } - if (isTooBigForAutoScrolling(droppable.viewport.frame, draggable.page.withMargin)) { + if (canScrollDroppable(droppable, request.scroll)) { + // not scheduling - jump requests need to be performed instantly + scrollDroppable(droppable.descriptor.id, request.scroll); return; } // not scheduling - jump requests need to be performed instantly - scrollDroppable(destination.droppableId, request.scroll); + scrollWindow(request.scroll); }; const onStateChange = (previous: State, current: State): void => { diff --git a/src/state/dimension.js b/src/state/dimension.js index b9a02c2274..37369b7a1a 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -1,8 +1,8 @@ // @flow import { vertical, horizontal } from './axis'; import getArea from './get-area'; -import { add, offset } from './spacing'; -import { subtract, negate } from './position'; +import { offsetByPosition, expandBySpacing } from './spacing'; +import { add, subtract, negate } from './position'; import type { DraggableDescriptor, DroppableDescriptor, @@ -13,6 +13,7 @@ import type { Spacing, Area, DroppableDimensionViewport, + ClosestScrollable, } from '../types'; const origin: Position = { x: 0, y: 0 }; @@ -24,28 +25,6 @@ export const noSpacing: Spacing = { left: 0, }; -const addPosition = (area: Area, point: Position): Area => { - const { top, right, bottom, left } = area; - return getArea({ - top: top + point.y, - left: left + point.x, - bottom: bottom + point.y, - right: right + point.x, - }); -}; - -const addSpacing = (area: Area, spacing: Spacing): Area => { - const { top, right, bottom, left } = area; - return getArea({ - // pulling back to increase size - top: top - spacing.top, - left: left - spacing.left, - // pushing forward to increase size - bottom: bottom + spacing.bottom, - right: right + spacing.right, - }); -}; - type GetDraggableArgs = {| descriptor: DraggableDescriptor, client: Area, @@ -59,7 +38,7 @@ export const getDraggableDimension = ({ margin = noSpacing, windowScroll = origin, }: GetDraggableArgs): DraggableDimension => { - const withScroll = addPosition(client, windowScroll); + const withScroll = offsetByPosition(client, windowScroll); const dimension: DraggableDimension = { descriptor, @@ -73,12 +52,12 @@ export const getDraggableDimension = ({ // on the viewport client: { withoutMargin: getArea(client), - withMargin: getArea(addSpacing(client, margin)), + withMargin: getArea(expandBySpacing(client, margin)), }, // with scroll page: { withoutMargin: getArea(withScroll), - withMargin: getArea(addSpacing(withScroll, margin)), + withMargin: getArea(expandBySpacing(withScroll, margin)), }, }; @@ -89,8 +68,13 @@ type GetDroppableArgs = {| descriptor: DroppableDescriptor, client: Area, // optionally provided - and can also be null - frameClient?: ?Area, - frameScroll?: Position, + closest: ?{| + frameClient: Area, + scrollWidth: number, + scrollHeight: number, + scroll: Position, + shouldClipSubject: boolean, + |}, direction?: Direction, margin?: Spacing, padding?: Spacing, @@ -116,38 +100,87 @@ export const clip = (frame: Area, subject: Spacing): ?Area => { return result; }; +const getSmallestSignedValue = (value: number) => { + if (value === 0) { + return 0; + } + return value > 0 ? 1 : -1; +}; + +export const canScrollDroppable = ( + droppable: DroppableDimension, + newScroll: Position, +): boolean => { + const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; + + // Cannot scroll a droppable that does not have a scroll container + if (!closestScrollable) { + return false; + } + + const smallestChange: Position = { + x: getSmallestSignedValue(newScroll.x), + y: getSmallestSignedValue(newScroll.y), + }; + + const targetScroll: Position = add(smallestChange, closestScrollable.scroll.initial); + const max: Position = closestScrollable.scroll.max; + const min: Position = closestScrollable.scroll.min; + + if (targetScroll.y > max.y && targetScroll.x > max.x) { + return false; + } + + if (targetScroll.y < min.y && targetScroll.x < min.y) { + return false; + } + + return true; +}; + export const scrollDroppable = ( droppable: DroppableDimension, newScroll: Position ): DroppableDimension => { - const existing: DroppableDimensionViewport = droppable.viewport; - const frame: ?Area = existing.frame; - - if (frame == null) { - console.error('Cannot scroll Droppable that does not have a frame'); + if (!droppable.viewport.closestScrollable) { + console.error('Cannot scroll droppble that does not have a closest scrollable'); return droppable; } - const scrollDiff: Position = subtract(newScroll, existing.frameScroll.initial); + const existingScrollable: ClosestScrollable = droppable.viewport.closestScrollable; + + const frame: Area = existingScrollable.frame; + + const scrollDiff: Position = subtract(newScroll, existingScrollable.scroll.initial); // a positive scroll difference leads to a negative displacement // (scrolling down pulls an item upwards) const scrollDisplacement: Position = negate(scrollDiff); - const displacedSubject: Spacing = offset(existing.subject, scrollDisplacement); - const clipped: ?Area = clip(frame, displacedSubject); - const viewport: DroppableDimensionViewport = { - // does not change - frame: existing.frame, - subject: existing.subject, - // below here changes - frameScroll: { - initial: existing.frameScroll.initial, + const closestScrollable: ClosestScrollable = { + frame: existingScrollable.frame, + shouldClipSubject: existingScrollable.shouldClipSubject, + scroll: { + initial: existingScrollable.scroll.initial, current: newScroll, diff: { value: scrollDiff, displacement: scrollDisplacement, }, + min: existingScrollable.scroll.min, + max: existingScrollable.scroll.max, }, + }; + + const displacedSubject: Spacing = + offsetByPosition(droppable.viewport.subject, scrollDisplacement); + + const clipped: ?Area = closestScrollable.shouldClipSubject ? + clip(frame, displacedSubject) : + getArea(displacedSubject); + + const viewport: DroppableDimensionViewport = { + closestScrollable, + subject: droppable.viewport.subject, clipped, }; @@ -161,39 +194,66 @@ export const scrollDroppable = ( export const getDroppableDimension = ({ descriptor, client, - frameClient, - frameScroll = origin, + closest, direction = 'vertical', margin = noSpacing, padding = noSpacing, windowScroll = origin, isEnabled = true, }: GetDroppableArgs): DroppableDimension => { - const withMargin = addSpacing(client, margin); - const withWindowScroll = addPosition(client, windowScroll); + const withMargin: Spacing = expandBySpacing(client, margin); + const withWindowScroll: Spacing = offsetByPosition(client, windowScroll); // If no frameClient is provided, or if the area matches the frameClient, this // droppable is its own container. In this case we include its margin in the container bounds. // Otherwise, the container is a scrollable parent. In this case we don't care about margins // in the container bounds. - const subject: Area = addSpacing(withWindowScroll, margin); + const subject: Area = getArea(expandBySpacing(withWindowScroll, margin)); - // use client + margin if frameClient is not provided - const frame: ?Area = frameClient ? addPosition(frameClient, windowScroll) : null; + const closestScrollable: ?ClosestScrollable = (() => { + if (!closest) { + return null; + } - const viewport: DroppableDimensionViewport = { - frame, - frameScroll: { - initial: frameScroll, - // no scrolling yet, so current = initial - current: frameScroll, - diff: { - value: origin, - displacement: origin, + const frame: Area = getArea(offsetByPosition(closest.frameClient, windowScroll)); + + const minScroll: Position = { + x: frame.left, + y: frame.top, + }; + + const maxScroll: Position = add(minScroll, { + x: closest.scrollWidth, + y: closest.scrollHeight, + }); + + const result: ClosestScrollable = { + frame, + shouldClipSubject: closest.shouldClipSubject, + scroll: { + initial: closest.scroll, + // no scrolling yet, so current = initial + current: closest.scroll, + min: minScroll, + max: maxScroll, + diff: { + value: origin, + displacement: origin, + }, }, - }, + }; + + return result; + })(); + + const clipped: ?Area = (closestScrollable && closestScrollable.shouldClipSubject) ? + clip(closestScrollable.frame, subject) : + subject; + + const viewport: DroppableDimensionViewport = { + closestScrollable, subject, - clipped: frame ? clip(frame, subject) : subject, + clipped, }; const dimension: DroppableDimension = { @@ -203,12 +263,13 @@ export const getDroppableDimension = ({ client: { withoutMargin: getArea(client), withMargin: getArea(withMargin), - withMarginAndPadding: getArea(addSpacing(withMargin, padding)), + withMarginAndPadding: getArea(expandBySpacing(withMargin, padding)), }, page: { withoutMargin: getArea(withWindowScroll), withMargin: subject, - withMarginAndPadding: getArea(addSpacing(withWindowScroll, add(margin, padding))), + withMarginAndPadding: + getArea(expandBySpacing(withWindowScroll, expandBySpacing(margin, padding))), }, viewport, }; diff --git a/src/state/spacing.js b/src/state/spacing.js index e7056da3bb..76dd658fd4 100644 --- a/src/state/spacing.js +++ b/src/state/spacing.js @@ -4,16 +4,27 @@ import type { Spacing, } from '../types'; -// expands a spacing -export const add = (spacing1: Spacing, spacing2: Spacing): Spacing => ({ - top: spacing1.top + spacing2.top, - left: spacing1.left + spacing2.left, - right: spacing1.right + spacing2.right, +export const offsetByPosition = (spacing: Spacing, point: Position): Spacing => ({ + top: spacing.top + point.y, + left: spacing.left + point.x, + bottom: spacing.bottom + point.y, + right: spacing.right + point.x, +}); + +export const expandBySpacing = (spacing1: Spacing, spacing2: Spacing): Spacing => ({ + // pulling back to increase size + top: spacing1.top - spacing2.top, + left: spacing1.left - spacing2.left, + // pushing forward to increase size bottom: spacing1.bottom + spacing2.bottom, + right: spacing1.right + spacing2.right, }); -export const addPosition = (spacing: Spacing, position: Position): Spacing => ({ - ...spacing, +export const expandByPosition = (spacing: Spacing, position: Position): Spacing => ({ + // pulling back to increase size + top: spacing.top - position.y, + left: spacing.left - position.x, + // pushing forward to increase size right: spacing.right + position.x, bottom: spacing.bottom + position.y, }); @@ -25,13 +36,6 @@ export const isEqual = (spacing1: Spacing, spacing2: Spacing): boolean => ( spacing1.left === spacing2.left ); -export const offset = (spacing: Spacing, point: Position): Spacing => ({ - top: spacing.top + point.y, - right: spacing.right + point.x, - bottom: spacing.bottom + point.y, - left: spacing.left + point.x, -}); - export const getCorners = (spacing: Spacing): Position[] => [ { x: spacing.left, y: spacing.top }, { x: spacing.right, y: spacing.top }, diff --git a/src/types.js b/src/types.js index 931720e786..49ea8f12ca 100644 --- a/src/types.js +++ b/src/types.js @@ -98,15 +98,20 @@ export type DraggableDimension = {| |}, |} -export type DroppableDimensionViewport = {| +export type ClosestScrollable = {| // This is the window through which the droppable is observed // It does not change during a drag - // frame is null when the droppable has no frame - frame: ?Area, - // keeping track of the scroll - frameScroll: {| + frame: Area, + // Whether or not we should clip the subject by the frame + // Is controlled by the ignoreContainerClipping prop + shouldClipSubject: boolean, + scroll: {| initial: Position, current: Position, + // the minimum allowable scroll for the frame + min: Position, + // the maxium allowable scroll for the frame + max: Position, diff: {| value: Position, // The actual displacement as a result of a scroll is in the opposite @@ -114,13 +119,16 @@ export type DroppableDimensionViewport = {| // upwards. This value is the negated version of the 'value' displacement: Position, |} - |}, - // The area to be clipped by the frame - // This is the initial capture of the subject and is not updated + |} +|} + +export type DroppableDimensionViewport = {| + // will be null if there is no closest scrollable + closestScrollable: ?ClosestScrollable, subject: Area, - // this is the subject through the viewport of the frame + // this is the subject through the viewport of the frame (if applicable) // it also takes into account any changes to the viewport scroll - // clipped area will be null if it is completely outside of the frame + // clipped area will be null if it is completely outside of the frame and frame clipping is on clipped: ?Area, |} diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 354629dfcd..d4bef1c5cf 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -259,25 +259,33 @@ export default class DroppableDimensionPublisher extends Component { // 2. There is no scroll container // 3. The droppable has internal scrolling - const frameClient: ?Area = (() => { - if (ignoreContainerClipping) { - return null; - } - if (!this.closestScrollable) { - return null; - } - if (this.closestScrollable === targetRef) { + const closest = (() => { + const closestScrollable: ?Element = this.closestScrollable; + + if (!closestScrollable) { return null; } - return getArea(this.closestScrollable.getBoundingClientRect()); + + // TODO: add margin? + const frameClient: Area = getArea(closestScrollable.getBoundingClientRect()); + const scrollWidth: number = closestScrollable.scrollWidth; + const scrollHeight: number = closestScrollable.scrollHeight; + const scroll: Position = this.getClosestScroll(); + + return { + frameClient, + scrollWidth, + scrollHeight, + scroll, + shouldClipSubject: !this.props.ignoreContainerClipping, + }; })(); const dimension: DroppableDimension = getDroppableDimension({ descriptor, direction, client, - frameClient, - frameScroll, + closest, margin, padding, windowScroll: getWindowScrollPosition(), From 6f87401e65e20998c8bb53f87ec4c41c6fc92af5 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 30 Jan 2018 10:20:29 +1100 Subject: [PATCH 029/163] responding to type refactor --- src/state/action-creators.js | 4 +-- .../auto-scroll-marshal.js | 19 +++++------ ...er.js => get-scrollable-droppable-over.js} | 15 ++++---- .../auto-scroll-marshal/scroll-window.js | 4 +-- src/state/get-displacement.js | 2 +- src/state/get-drag-impact/in-foreign-list.js | 8 +++-- src/state/get-drag-impact/in-home-list.js | 6 +++- src/state/get-droppable-over.js | 16 ++++++--- .../get-best-cross-axis-droppable.js | 12 +++---- .../move-cross-axis/get-closest-draggable.js | 2 +- src/state/move-to-next-index/get-result.js | 21 +++++------- .../move-to-next-index/in-foreign-list.js | 1 - src/state/move-to-next-index/in-home-list.js | 27 ++++++--------- .../is-totally-visible-in-new-location.js | 7 ++-- .../move-to-next-index-types.js | 3 +- src/state/reducer.js | 3 +- src/state/visibility/is-visible.js | 34 ++++++------------- src/types.js | 7 +--- .../droppable-dimension-publisher.jsx | 5 ++- 19 files changed, 88 insertions(+), 108 deletions(-) rename src/state/auto-scroll-marshal/{get-droppable-frame-over.js => get-scrollable-droppable-over.js} (70%) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 087c12aec5..8863de3298 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -39,8 +39,8 @@ const getScrollDiff = ({ current.windowScroll ); - const droppableScrollDiff: Position = droppable ? - droppable.viewport.frameScroll.diff.displacement : + const droppableScrollDiff: Position = droppable && droppable.viewport.closestScrollable ? + droppable.viewport.closestScrollable.scroll.diff.displacement : origin; return add(windowScrollDiff, droppableScrollDiff); diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 63a9cf7e5c..1227df5067 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -3,7 +3,7 @@ import rafSchd from 'raf-schd'; import getViewport from '../visibility/get-viewport'; import { isEqual } from '../position'; import { vertical, horizontal } from '../axis'; -import getDroppableFrameOver from './get-droppable-frame-over'; +import getScrollableDroppableOver from './get-scrollable-droppable-over'; import { canScrollDroppable } from '../dimension'; import scrollWindow, { canScroll as canScrollWindow } from './scroll-window'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; @@ -18,7 +18,6 @@ import type { Spacing, DraggableLocation, DraggableDimension, - ScrollJumpRequest, } from '../../types'; type Args = {| @@ -167,7 +166,7 @@ export default ({ // 2. We are not scrolling the window. Can we scroll the Droppable? - const droppable: ?DroppableDimension = getDroppableFrameOver({ + const droppable: ?DroppableDimension = getScrollableDroppableOver({ target: center, droppables: state.dimension.droppable, }); @@ -202,7 +201,7 @@ export default ({ return; } - const request: ?ScrollJumpRequest = drag.scrollJumpRequest; + const request: ?Position = drag.scrollJumpRequest; if (!request) { return; @@ -217,8 +216,8 @@ export default ({ } const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; - - if (droppable.viewport.clipped == null) { + const clipped: ?Area = droppable.viewport.clipped; + if (clipped == null) { return; } @@ -226,18 +225,18 @@ export default ({ return; } - if (isTooBigForAutoScrolling(droppable.viewport.clipped, draggable.page.withMargin)) { + if (isTooBigForAutoScrolling(clipped, draggable.page.withMargin)) { return; } - if (canScrollDroppable(droppable, request.scroll)) { + if (canScrollDroppable(droppable, request)) { // not scheduling - jump requests need to be performed instantly - scrollDroppable(droppable.descriptor.id, request.scroll); + scrollDroppable(droppable.descriptor.id, request); return; } // not scheduling - jump requests need to be performed instantly - scrollWindow(request.scroll); + scrollWindow(request); }; const onStateChange = (previous: State, current: State): void => { diff --git a/src/state/auto-scroll-marshal/get-droppable-frame-over.js b/src/state/auto-scroll-marshal/get-scrollable-droppable-over.js similarity index 70% rename from src/state/auto-scroll-marshal/get-droppable-frame-over.js rename to src/state/auto-scroll-marshal/get-scrollable-droppable-over.js index a77ea51f65..c2378e867e 100644 --- a/src/state/auto-scroll-marshal/get-droppable-frame-over.js +++ b/src/state/auto-scroll-marshal/get-scrollable-droppable-over.js @@ -2,7 +2,6 @@ import memoizeOne from 'memoize-one'; import isPositionInFrame from '../visibility/is-position-in-frame'; import type { - Area, DroppableDimension, DroppableDimensionMap, DroppableId, @@ -14,7 +13,7 @@ type Args = {| droppables: DroppableDimensionMap, |}; -const getDroppablesWithAFrame = memoizeOne( +const getScrollableDroppables = memoizeOne( (droppables: DroppableDimensionMap): DroppableDimension[] => ( Object.keys(droppables) .map((id: DroppableId): DroppableDimension => droppables[id]) @@ -24,8 +23,8 @@ const getDroppablesWithAFrame = memoizeOne( return false; } - // only want droppables that have a frame - if (!droppable.viewport.frame) { + // only want droppables that are scrollable + if (!droppable.viewport.closestScrollable) { return false; } @@ -39,10 +38,12 @@ export default ({ droppables, }: Args): ?DroppableDimension => { const overDroppablesFrame: ?DroppableDimension = - getDroppablesWithAFrame(droppables) + getScrollableDroppables(droppables) .find((droppable: DroppableDimension): boolean => { - const frame: Area = (droppable.viewport.frame: any); - return isPositionInFrame(frame)(target); + if (!droppable.viewport.closestScrollable) { + throw new Error('Invalid result'); + } + return isPositionInFrame(droppable.viewport.closestScrollable.frame)(target); }); return overDroppablesFrame; diff --git a/src/state/auto-scroll-marshal/scroll-window.js b/src/state/auto-scroll-marshal/scroll-window.js index 02f6963152..3825ad6eb4 100644 --- a/src/state/auto-scroll-marshal/scroll-window.js +++ b/src/state/auto-scroll-marshal/scroll-window.js @@ -1,5 +1,5 @@ // @flow -import { offset } from '../spacing'; +import { offsetByPosition } from '../spacing'; import getViewport from '../visibility/get-viewport'; import type { Area, @@ -24,7 +24,7 @@ export const canScroll = (change: Position): boolean => { y: getSmallestSignedValue(change.y), }; - const shifted: Spacing = offset(viewport, smallestChange); + const shifted: Spacing = offsetByPosition(viewport, smallestChange); // TEMP // if (shifted.left === 0 && shifted.top === 0) { diff --git a/src/state/get-displacement.js b/src/state/get-displacement.js index 45b13ed6ee..80d7e16b0e 100644 --- a/src/state/get-displacement.js +++ b/src/state/get-displacement.js @@ -31,7 +31,7 @@ export default ({ target: draggable.page.withMargin, destination, viewport, - }).isVisible; + }); const shouldAnimate: boolean = (() => { // if should be displaced and not visible diff --git a/src/state/get-drag-impact/in-foreign-list.js b/src/state/get-drag-impact/in-foreign-list.js index cc8d537d4a..dafe6b608f 100644 --- a/src/state/get-drag-impact/in-foreign-list.js +++ b/src/state/get-drag-impact/in-foreign-list.js @@ -21,6 +21,8 @@ type Args = {| previousImpact: DragImpact, |} +const origin: Position = { x: 0, y: 0 }; + export default ({ pageCenter, draggable, @@ -36,8 +38,10 @@ export default ({ // To do this we need to consider any displacement caused by // a change in scroll in the droppable we are currently over. - const destinationScrollDiff: Position = - destination.viewport.frameScroll.diff.value; + const destinationScrollDiff: Position = destination.viewport.closestScrollable ? + destination.viewport.closestScrollable.scroll.diff.value : + origin; + const currentCenter: Position = add(pageCenter, destinationScrollDiff); const displaced: Displacement[] = insideDestination diff --git a/src/state/get-drag-impact/in-home-list.js b/src/state/get-drag-impact/in-home-list.js index 4c18977c78..49cd3b37a6 100644 --- a/src/state/get-drag-impact/in-home-list.js +++ b/src/state/get-drag-impact/in-home-list.js @@ -24,6 +24,8 @@ type Args = {| previousImpact: DragImpact, |} +const origin: Position = { x: 0, y: 0 }; + export default ({ pageCenter, draggable, @@ -39,7 +41,9 @@ export default ({ // Where is the element now? // Need to take into account the change of scroll in the droppable - const homeScrollDiff: Position = home.viewport.frameScroll.diff.value; + const homeScrollDiff: Position = home.viewport.closestScrollable ? + home.viewport.closestScrollable.scroll.diff.value : + origin; // Where the element actually is now const currentCenter: Position = add(pageCenter, homeScrollDiff); diff --git a/src/state/get-droppable-over.js b/src/state/get-droppable-over.js index fe681f8205..9d89127349 100644 --- a/src/state/get-droppable-over.js +++ b/src/state/get-droppable-over.js @@ -4,9 +4,10 @@ import getArea from './get-area'; import getDraggablesInsideDroppable from './get-draggables-inside-droppable'; import isPositionInFrame from './visibility/is-position-in-frame'; import { patch } from './position'; -import { addPosition } from './spacing'; +import { expandByPosition } from './spacing'; import { clip } from './dimension'; import type { + ClosestScrollable, DraggableDimension, DraggableDimensionMap, DroppableDimension, @@ -65,7 +66,7 @@ type GetBufferedDroppableArgs = { }; const getWithGrowth = memoizeOne( - (area: Area, growth: Position): Area => getArea(addPosition(area, growth)) + (area: Area, growth: Position): Area => getArea(expandByPosition(area, growth)) ); const getClippedAreaWithPlaceholder = ({ @@ -79,7 +80,6 @@ const getClippedAreaWithPlaceholder = ({ previousDroppableOverId && previousDroppableOverId === droppable.descriptor.id ); - const frame: ?Area = droppable.viewport.frame; const clipped: ?Area = droppable.viewport.clipped; // clipped area is totally hidden behind frame @@ -100,15 +100,21 @@ const getClippedAreaWithPlaceholder = ({ } const subjectWithGrowth = getWithGrowth(clipped, requiredGrowth); + const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; // The droppable has no scroll container - if (!frame) { + if (!closestScrollable) { + return subjectWithGrowth; + } + + // We are not clipping the subject + if (!closestScrollable.shouldClipSubject) { return subjectWithGrowth; } // We need to clip the new subject by the frame which does not change // This will allow the user to continue to scroll into the placeholder - return clip(frame, subjectWithGrowth); + return clip(closestScrollable.frame, subjectWithGrowth); }; type Args = {| diff --git a/src/state/move-cross-axis/get-best-cross-axis-droppable.js b/src/state/move-cross-axis/get-best-cross-axis-droppable.js index e8b0233930..49ebed3d67 100644 --- a/src/state/move-cross-axis/get-best-cross-axis-droppable.js +++ b/src/state/move-cross-axis/get-best-cross-axis-droppable.js @@ -58,16 +58,14 @@ export default ({ .filter((droppable: DroppableDimension): boolean => droppable !== source) // Remove any options that are not enabled .filter((droppable: DroppableDimension): boolean => droppable.isEnabled) - // Remove any droppables that have invisible subjects - .filter((droppable: DroppableDimension): boolean => Boolean(droppable.viewport.clipped)) // Remove any droppables that are not partially visible .filter((droppable: DroppableDimension): boolean => { - const frame: ?Area = droppable.viewport.frame; - // Droppable has no scroll container - if (!frame) { - return true; + const clipped: ?Area = droppable.viewport.clipped; + // subject is not visible + if (!clipped) { + return false; } - return isPartiallyVisibleThroughFrame(viewport)(frame); + return isPartiallyVisibleThroughFrame(viewport)(clipped); }) .filter((droppable: DroppableDimension): boolean => { const targetClipped: Area = getSafeClipped(droppable); diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index 093a224e76..b328a0eb87 100644 --- a/src/state/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-cross-axis/get-closest-draggable.js @@ -40,7 +40,7 @@ export default ({ target: draggable.page.withMargin, destination, viewport, - }).isVisible) + })) .sort((a: DraggableDimension, b: DraggableDimension): number => { // TODO: need to consider droppable scroll const distanceToA = distance(pageCenter, a.page.withMargin.center); diff --git a/src/state/move-to-next-index/get-result.js b/src/state/move-to-next-index/get-result.js index 8495b0359d..b898a277ad 100644 --- a/src/state/move-to-next-index/get-result.js +++ b/src/state/move-to-next-index/get-result.js @@ -3,13 +3,11 @@ import { subtract } from '../position'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import getViewport from '../visibility/get-viewport'; import type { Result } from './move-to-next-index-types'; -import type { IsVisibleResult } from '../visibility/is-visible'; import type { DroppableDimension, DraggableDimension, Position, DragImpact, - ScrollJumpRequest, } from '../../types'; type Args = {| @@ -20,6 +18,8 @@ type Args = {| newImpact: DragImpact, |} +const origin: Position = { x: 0, y: 0 }; + export default ({ destination, draggable, @@ -27,15 +27,17 @@ export default ({ newPageCenter, newImpact, }: Args): Result => { - const isTotallyVisible: IsVisibleResult = isTotallyVisibleInNewLocation({ + const isTotallyVisible: boolean = isTotallyVisibleInNewLocation({ draggable, destination, newPageCenter, viewport: getViewport(), }); + const scrollDiff: Position = destination.viewport.closestScrollable ? + destination.viewport.closestScrollable.scroll.diff.value : + origin; - if (isTotallyVisible.isVisible) { - const scrollDiff: Position = destination.viewport.frameScroll.diff.value; + if (isTotallyVisible) { const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); return { @@ -49,21 +51,14 @@ export default ({ const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); // We need to consider how much the droppable scroll has changed - const scrollDiff: Position = destination.viewport.frameScroll.diff.value; - // The actual scroll required to move into the next place const requiredScroll: Position = subtract(requiredDistance, scrollDiff); - const request: ScrollJumpRequest = { - scroll: requiredScroll, - target: isTotallyVisible.isVisibleInDroppable ? 'WINDOW' : 'DROPPABLE', - }; - return { // Using the previous page center with a new impact // as we are not visually moving the Draggable pageCenter: previousPageCenter, impact: newImpact, - scrollJumpRequest: request, + scrollJumpRequest: requiredScroll, }; }; diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index ed6320a245..12982ecf7d 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -8,7 +8,6 @@ import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location' import getResult from './get-result'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; -import type { IsVisibleResult } from '../visibility/is-visible'; import type { DraggableLocation, DraggableDimension, diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 482322c8b0..65ecb944f8 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -2,13 +2,11 @@ import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import { subtract, patch } from '../position'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; -import type { IsVisibleResult } from '../visibility/is-visible'; import getViewport from '../visibility/get-viewport'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import getDisplacement from '../get-displacement'; -import { isTotallyVisible } from '../visibility/is-visible'; import type { DraggableLocation, DraggableDimension, @@ -17,9 +15,10 @@ import type { Axis, DragImpact, Area, - ScrollJumpRequest, } from '../../types'; +const origin: Position = { x: 0, y: 0 }; + export default ({ isMovingForward, draggableId, @@ -83,37 +82,31 @@ export default ({ destinationAxis: droppable.axis, }); - const newLocationVisibility: IsVisibleResult = isTotallyVisibleInNewLocation({ + const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ draggable, destination: droppable, newPageCenter, viewport: getViewport(), }); - if (!newLocationVisibility.isVisible) { + if (!isVisibleInNewLocation) { // The full distance required to get from the previous page center to the new page center const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); // We need to consider how much the droppable scroll has changed - const scrollDiff: Position = droppable.viewport.frameScroll.diff.value; + const scrollDiff: Position = droppable.viewport.closestScrollable ? + droppable.viewport.closestScrollable.scroll.diff.value : + origin; // The actual scroll required to move into the next place const requiredScroll: Position = subtract(requiredDistance, scrollDiff); - // need to prioritise scrolling a droppable so that we do not leave its boundaries - const toBeScrolled = newLocationVisibility.isVisibleInDroppable ? 'ANY' : 'DROPPABLE'; - - const request: ScrollJumpRequest = { - scroll: requiredScroll, - toBeScrolled, - }; - return { // Using the previous page center with a new impact // as we are not visually moving the Draggable pageCenter: previousPageCenter, impact: previousImpact, - scrollJumpRequest: request, + scrollJumpRequest: requiredScroll, }; } @@ -162,7 +155,9 @@ export default ({ direction: droppable.axis.direction, }; - const scrollDiff: Position = droppable.viewport.frameScroll.diff.value; + const scrollDiff: Position = droppable.viewport.closestScrollable ? + droppable.viewport.closestScrollable.scroll.diff.value : + origin; const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); return { diff --git a/src/state/move-to-next-index/is-totally-visible-in-new-location.js b/src/state/move-to-next-index/is-totally-visible-in-new-location.js index 1cffe212ed..0f3a3130ac 100644 --- a/src/state/move-to-next-index/is-totally-visible-in-new-location.js +++ b/src/state/move-to-next-index/is-totally-visible-in-new-location.js @@ -1,8 +1,7 @@ // @flow import { subtract } from '../position'; -import { offset } from '../spacing'; +import { offsetByPosition } from '../spacing'; import { isTotallyVisible } from '../visibility/is-visible'; -import type { IsVisibleResult } from '../visibility/is-visible'; import type { Area, DraggableDimension, @@ -23,13 +22,13 @@ export default ({ destination, newPageCenter, viewport, -}: Args): IsVisibleResult => { +}: Args): boolean => { // What would the location of the Draggable be once the move is completed? // We are not considering margins for this calculation. // This is because a move might move a Draggable slightly outside of the bounds // of a Droppable (which is okay) const diff: Position = subtract(newPageCenter, draggable.page.withoutMargin.center); - const shifted: Spacing = offset(draggable.page.withoutMargin, diff); + const shifted: Spacing = offsetByPosition(draggable.page.withoutMargin, diff); // Must be totally visible, not just partially visible. diff --git a/src/state/move-to-next-index/move-to-next-index-types.js b/src/state/move-to-next-index/move-to-next-index-types.js index b9f8344460..3c60dccae1 100644 --- a/src/state/move-to-next-index/move-to-next-index-types.js +++ b/src/state/move-to-next-index/move-to-next-index-types.js @@ -5,7 +5,6 @@ import type { DragImpact, DroppableDimension, DraggableDimensionMap, - ScrollJumpRequest, } from '../../types'; export type Args = {| @@ -25,5 +24,5 @@ export type Result = {| // Any scroll that is required for the movement. // If this is present then the pageCenter and impact // will just be the same as the previous drag - scrollJumpRequest: ?ScrollJumpRequest, + scrollJumpRequest: ?Position, |} diff --git a/src/state/reducer.js b/src/state/reducer.js index 7195d329e4..1407defe36 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -22,7 +22,6 @@ import type { CurrentDragPositions, Position, InitialDragPositions, - ScrollJumpRequest, } from '../types'; import { add, subtract, isEqual } from './position'; import { noMovement } from './no-impact'; @@ -56,7 +55,7 @@ type MoveArgs = {| // force a custom drag impact (optionally provided) impact?: DragImpact, // provide a scroll jump request (optionally provided - and can be null) - scrollJumpRequest?: ?ScrollJumpRequest, + scrollJumpRequest?: ?Position, |} const canPublishDimension = (phase: Phase): boolean => diff --git a/src/state/visibility/is-visible.js b/src/state/visibility/is-visible.js index 7037d66454..0bb8b6d782 100644 --- a/src/state/visibility/is-visible.js +++ b/src/state/visibility/is-visible.js @@ -1,7 +1,7 @@ // @flow import isPartiallyVisibleThroughFrame from './is-partially-visible-through-frame'; import isTotallyVisibleThroughFrame from './is-totally-visible-through-frame'; -import { offset } from '../spacing'; +import { offsetByPosition } from '../spacing'; import type { Spacing, Position, @@ -20,31 +20,23 @@ type HelperArgs = {| isVisibleThroughFrameFn: (frame: Spacing) => (subject: Spacing) => boolean |} -export type IsVisibleResult = {| - isVisible: boolean, - isVisibleInViewport: boolean, - isVisibleInDroppable: boolean, -|} - -const nope: IsVisibleResult = { - isVisible: false, - isVisibleInDroppable: false, - isVisibleInViewport: false, -}; +const origin: Position = { x: 0, y: 0 }; const isVisible = ({ target, destination, viewport, isVisibleThroughFrameFn, -}: HelperArgs): IsVisibleResult => { - const displacement: Position = destination.viewport.frameScroll.diff.displacement; - const withScroll: Spacing = offset(target, displacement); +}: HelperArgs): boolean => { + const displacement: Position = destination.viewport.closestScrollable ? + destination.viewport.closestScrollable.scroll.diff.displacement : + origin; + const withScroll: Spacing = offsetByPosition(target, displacement); // destination subject is totally hidden by frame // this should never happen - but just guarding against it if (!destination.viewport.clipped) { - return nope; + return false; } // When considering if the target is visible in the droppable we need @@ -59,18 +51,14 @@ const isVisible = ({ const isVisibleInViewport: boolean = isVisibleThroughFrameFn(viewport)(withScroll); - return { - isVisible: isVisibleInDroppable && isVisibleInViewport, - isVisibleInDroppable, - isVisibleInViewport, - }; + return isVisibleInDroppable && isVisibleInViewport; }; export const isPartiallyVisible = ({ target, destination, viewport, -}: Args): IsVisibleResult => isVisible({ +}: Args): boolean => isVisible({ target, destination, viewport, @@ -81,7 +69,7 @@ export const isTotallyVisible = ({ target, destination, viewport, -}: Args): IsVisibleResult => isVisible({ +}: Args): boolean => isVisible({ target, destination, viewport, diff --git a/src/types.js b/src/types.js index 49ea8f12ca..121072d654 100644 --- a/src/types.js +++ b/src/types.js @@ -246,17 +246,12 @@ export type DropResult = {| destination: ?DraggableLocation, |} -export type ScrollJumpRequest = {| - scroll: Position, - toBeScrolled: 'ANY' | 'DROPPABLE', -|} - export type DragState = {| initial: InitialDrag, current: CurrentDrag, impact: DragImpact, // if we need to jump the scroll (keyboard dragging) - scrollJumpRequest: ?ScrollJumpRequest, + scrollJumpRequest: ?Position, |} export type DropTrigger = 'DROP' | 'CANCEL'; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index d4bef1c5cf..8497fd4762 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -96,7 +96,7 @@ export default class DroppableDimensionPublisher extends Component { return; } - console.log('actually scrolling droppable', change); + console.log('DroppableDimensionPublisher: now scrolling', change); this.closestScrollable.scrollTop += change.y; this.closestScrollable.scrollLeft += change.x; @@ -233,7 +233,6 @@ export default class DroppableDimensionPublisher extends Component { // side effect - grabbing it for scroll listening so we know it is the same node this.closestScrollable = getClosestScrollable(targetRef); - const frameScroll: Position = this.getClosestScroll(); const style: Object = window.getComputedStyle(targetRef); // keeping it simple and always using the margin of the droppable @@ -266,7 +265,7 @@ export default class DroppableDimensionPublisher extends Component { return null; } - // TODO: add margin? + // TODO: add margin to client? const frameClient: Area = getArea(closestScrollable.getBoundingClientRect()); const scrollWidth: number = closestScrollable.scrollWidth; const scrollHeight: number = closestScrollable.scrollHeight; From a3a30371eefb284dbb9469744fb6437f572c9c58 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 30 Jan 2018 16:06:08 +1100 Subject: [PATCH 030/163] streamlined scroll logic checks --- .../auto-scroll-marshal.js | 50 +++++---- src/state/auto-scroll-marshal/can-scroll.js | 104 ++++++++++++++++++ .../get-scrollable-droppable-over.js | 4 +- .../auto-scroll-marshal/scroll-window.js | 56 +--------- src/state/dimension.js | 56 ++-------- src/state/get-max-scroll.js | 33 ++++++ src/types.js | 4 +- .../droppable-dimension-publisher.jsx | 4 + 8 files changed, 183 insertions(+), 128 deletions(-) create mode 100644 src/state/auto-scroll-marshal/can-scroll.js create mode 100644 src/state/get-max-scroll.js diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 1227df5067..5ffec323df 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -4,8 +4,8 @@ import getViewport from '../visibility/get-viewport'; import { isEqual } from '../position'; import { vertical, horizontal } from '../axis'; import getScrollableDroppableOver from './get-scrollable-droppable-over'; -import { canScrollDroppable } from '../dimension'; -import scrollWindow, { canScroll as canScrollWindow } from './scroll-window'; +import { canScrollDroppable, canScrollWindow } from './can-scroll'; +import scrollWindow from './scroll-window'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; import type { Area, @@ -18,6 +18,7 @@ import type { Spacing, DraggableLocation, DraggableDimension, + ClosestScrollable, } from '../../types'; type Args = {| @@ -171,26 +172,28 @@ export default ({ droppables: state.dimension.droppable, }); + // No scrollable targets if (!droppable) { return; } - // not a scrollable droppable (should not occur) - if (!droppable.viewport.frame) { - return; - } + // We know this has a closestScrollable + const closestScrollable: ClosestScrollable = (droppable.viewport.closestScrollable : any); - if (isTooBigForAutoScrolling(droppable.viewport.frame, draggable.page.withMargin)) { + if (isTooBigForAutoScrolling(closestScrollable.frame, draggable.page.withMargin)) { return; } - const requiredFrameScroll: ?Position = - getRequiredScroll(droppable.viewport.frame, center); + const requiredFrameScroll: ?Position = getRequiredScroll(closestScrollable.frame, center); if (!requiredFrameScroll) { return; } + if (!canScrollDroppable(droppable, requiredFrameScroll)) { + return; + } + scheduleDroppableScroll(droppable.descriptor.id, requiredFrameScroll); }; @@ -216,23 +219,30 @@ export default ({ } const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; - const clipped: ?Area = droppable.viewport.clipped; - if (clipped == null) { - return; - } + const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; - if (isTooBigForAutoScrolling(getViewport(), draggable.page.withMargin)) { - return; + if (closestScrollable) { + if (isTooBigForAutoScrolling(closestScrollable.frame, draggable.page.withMargin)) { + return; + } + + if (canScrollDroppable(droppable, request)) { + // not scheduling - jump requests need to be performed instantly + scrollDroppable(droppable.descriptor.id, request); + return; + } + + // can now check if we need to scroll the window } - if (isTooBigForAutoScrolling(clipped, draggable.page.withMargin)) { + // Scroll the window if we can + + if (isTooBigForAutoScrolling(getViewport(), draggable.page.withMargin)) { return; } - if (canScrollDroppable(droppable, request)) { - // not scheduling - jump requests need to be performed instantly - scrollDroppable(droppable.descriptor.id, request); - return; + if (!canScrollWindow(request)) { + console.warn('Jump scroll requested but it cannot be done by Droppable or the Window'); } // not scheduling - jump requests need to be performed instantly diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll-marshal/can-scroll.js new file mode 100644 index 0000000000..9d4bab153f --- /dev/null +++ b/src/state/auto-scroll-marshal/can-scroll.js @@ -0,0 +1,104 @@ +// @flow +import { add, isEqual, subtract } from '../position'; +// TODO: state reaching into VIEW :( +import getWindowScrollPosition from '../../view/get-window-scroll-position'; +import getViewport from '../visibility/get-viewport'; +import getMaxScroll from '../get-max-scroll'; +import type { + ClosestScrollable, + DroppableDimension, + Spacing, + Position, + Area, +} from '../../types'; + +type CanScrollArgs = {| + max: Position, + current: Position, + proposed: Position, +|} + +const origin: Position = { x: 0, y: 0 }; + +const getSmallestSignedValue = (value: number) => { + if (value === 0) { + return 0; + } + return value > 0 ? 1 : -1; +}; + +const canScroll = ({ + max, + current, + proposed, +}: CanScrollArgs): boolean => { + // Only need to be able to move the smallest amount in the desired direction + const smallestChange: Position = { + x: getSmallestSignedValue(proposed.x), + y: getSmallestSignedValue(proposed.y), + }; + + const target: Position = add(current, smallestChange); + + if (isEqual(target, origin)) { + return false; + } + + // Too far back + if (target.y <= 0 && target.x <= 0) { + return false; + } + + // Too far forward + if (target.y >= max.y && target.x >= max.x) { + return false; + } + + return true; +}; + +export const canScrollWindow = (change: Position): boolean => { + const el: ?HTMLElement = document.documentElement; + + if (!el) { + console.error('Cannot find document element'); + return false; + } + + const current: Position = getWindowScrollPosition(); + const viewport: Area = getViewport(); + + const maxScroll: Position = getMaxScroll({ + scrollHeight: el.scrollHeight, + scrollWidth: el.scrollWidth, + width: viewport.width, + height: viewport.height, + }); + + console.log('can scroll window?'); + + return canScroll({ + current, + max: maxScroll, + proposed: change, + }); +}; + +export const canScrollDroppable = ( + droppable: DroppableDimension, + change: Position, +): boolean => { + const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; + // Cannot scroll when there is no scroll container! + if (!closestScrollable) { + return false; + } + + console.log('can scroll droppable?'); + + return canScroll({ + current: closestScrollable.scroll.current, + max: closestScrollable.scroll.max, + proposed: change, + }); +}; diff --git a/src/state/auto-scroll-marshal/get-scrollable-droppable-over.js b/src/state/auto-scroll-marshal/get-scrollable-droppable-over.js index c2378e867e..ec60cf1500 100644 --- a/src/state/auto-scroll-marshal/get-scrollable-droppable-over.js +++ b/src/state/auto-scroll-marshal/get-scrollable-droppable-over.js @@ -37,7 +37,7 @@ export default ({ target, droppables, }: Args): ?DroppableDimension => { - const overDroppablesFrame: ?DroppableDimension = + const maybe: ?DroppableDimension = getScrollableDroppables(droppables) .find((droppable: DroppableDimension): boolean => { if (!droppable.viewport.closestScrollable) { @@ -46,5 +46,5 @@ export default ({ return isPositionInFrame(droppable.viewport.closestScrollable.frame)(target); }); - return overDroppablesFrame; + return maybe; }; diff --git a/src/state/auto-scroll-marshal/scroll-window.js b/src/state/auto-scroll-marshal/scroll-window.js index 3825ad6eb4..fd07f6e71a 100644 --- a/src/state/auto-scroll-marshal/scroll-window.js +++ b/src/state/auto-scroll-marshal/scroll-window.js @@ -1,63 +1,11 @@ // @flow -import { offsetByPosition } from '../spacing'; -import getViewport from '../visibility/get-viewport'; import type { - Area, Position, - Spacing, } from '../../types'; -const getSmallestSignedValue = (value: number) => { - if (value === 0) { - return 0; - } - return value > 0 ? 1 : -1; -}; - -// Will return true if can scroll even a little bit in either direction -// of the change. -export const canScroll = (change: Position): boolean => { - const viewport: Area = getViewport(); - // Only need to be able to move the smallest amount in the desired direction - const smallestChange: Position = { - x: getSmallestSignedValue(change.x), - y: getSmallestSignedValue(change.y), - }; - - const shifted: Spacing = offsetByPosition(viewport, smallestChange); - - // TEMP - // if (shifted.left === 0 && shifted.top === 0) { - // return true; - // } - - // moving back beyond origin - if (shifted.left <= 0 && shifted.top <= 0) { - return false; - } - - const el: ?HTMLElement = document.documentElement; - - if (!el) { - console.error('Cannot find document element'); - return false; - } - - // totally outside the full height of the page - if (shifted.right >= el.scrollWidth && shifted.bottom >= el.scrollHeight) { - return false; - } - - return true; -}; - // Not guarenteed to scroll by the entire amount export default (change: Position): void => { - if (canScroll(change)) { - console.log('scrolling window by ', change); - window.scrollBy(change.x, change.y); - } else { - console.log('cannot scroll window!', change); - } + console.log('scrolling window', change); + window.scrollBy(change.x, change.y); }; diff --git a/src/state/dimension.js b/src/state/dimension.js index 37369b7a1a..9ec914f900 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -2,7 +2,8 @@ import { vertical, horizontal } from './axis'; import getArea from './get-area'; import { offsetByPosition, expandBySpacing } from './spacing'; -import { add, subtract, negate } from './position'; +import { subtract, negate } from './position'; +import getMaxScroll from './get-max-scroll'; import type { DraggableDescriptor, DroppableDescriptor, @@ -100,44 +101,6 @@ export const clip = (frame: Area, subject: Spacing): ?Area => { return result; }; -const getSmallestSignedValue = (value: number) => { - if (value === 0) { - return 0; - } - return value > 0 ? 1 : -1; -}; - -export const canScrollDroppable = ( - droppable: DroppableDimension, - newScroll: Position, -): boolean => { - const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; - - // Cannot scroll a droppable that does not have a scroll container - if (!closestScrollable) { - return false; - } - - const smallestChange: Position = { - x: getSmallestSignedValue(newScroll.x), - y: getSmallestSignedValue(newScroll.y), - }; - - const targetScroll: Position = add(smallestChange, closestScrollable.scroll.initial); - const max: Position = closestScrollable.scroll.max; - const min: Position = closestScrollable.scroll.min; - - if (targetScroll.y > max.y && targetScroll.x > max.x) { - return false; - } - - if (targetScroll.y < min.y && targetScroll.x < min.y) { - return false; - } - - return true; -}; - export const scrollDroppable = ( droppable: DroppableDimension, newScroll: Position @@ -166,7 +129,6 @@ export const scrollDroppable = ( value: scrollDiff, displacement: scrollDisplacement, }, - min: existingScrollable.scroll.min, max: existingScrollable.scroll.max, }, }; @@ -217,14 +179,11 @@ export const getDroppableDimension = ({ const frame: Area = getArea(offsetByPosition(closest.frameClient, windowScroll)); - const minScroll: Position = { - x: frame.left, - y: frame.top, - }; - - const maxScroll: Position = add(minScroll, { - x: closest.scrollWidth, - y: closest.scrollHeight, + const maxScroll: Position = getMaxScroll({ + scrollHeight: closest.scrollHeight, + scrollWidth: closest.scrollWidth, + height: frame.height, + width: frame.width, }); const result: ClosestScrollable = { @@ -234,7 +193,6 @@ export const getDroppableDimension = ({ initial: closest.scroll, // no scrolling yet, so current = initial current: closest.scroll, - min: minScroll, max: maxScroll, diff: { value: origin, diff --git a/src/state/get-max-scroll.js b/src/state/get-max-scroll.js new file mode 100644 index 0000000000..fcdd4004fe --- /dev/null +++ b/src/state/get-max-scroll.js @@ -0,0 +1,33 @@ +// @flow +import { subtract } from './position'; +import type { Position } from '../types'; + +type Args = {| + scrollHeight: number, + scrollWidth: number, + height: number, + width: number, +|} +export default ({ + scrollHeight, + scrollWidth, + height, + width, +}: Args): Position => { + const maxScroll: Position = subtract( + // full size + { x: scrollWidth, y: scrollHeight }, + // viewport size + { x: width, y: height } + ); + + // Due to scroll bars sometimes the width / height can be greater + // than the scrollWidth / scrollHeight + const adjustedMaxScroll: Position = { + x: Math.max(0, maxScroll.x), + y: Math.max(0, maxScroll.y), + }; + + return adjustedMaxScroll; +}; + diff --git a/src/types.js b/src/types.js index 121072d654..ecce943b86 100644 --- a/src/types.js +++ b/src/types.js @@ -108,9 +108,7 @@ export type ClosestScrollable = {| scroll: {| initial: Position, current: Position, - // the minimum allowable scroll for the frame - min: Position, - // the maxium allowable scroll for the frame + // the maximum allowable scroll for the frame max: Position, diff: {| value: Position, diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 8497fd4762..a409c5879f 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -96,6 +96,10 @@ export default class DroppableDimensionPublisher extends Component { return; } + if (!this.isWatchingScroll) { + console.warn('Updating Droppable scroll while not watching for updates'); + } + console.log('DroppableDimensionPublisher: now scrolling', change); this.closestScrollable.scrollTop += change.y; From a4295fee66a3ddd40109182f8dd59ea5c6fdbe0e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 30 Jan 2018 17:07:59 +1100 Subject: [PATCH 031/163] using easing function for fluid scroll --- .../auto-scroll-marshal.js | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 5ffec323df..41330b0256 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -25,28 +25,35 @@ type Args = {| scrollDroppable: (id: DroppableId, change: Position) => void, |} +// Values used to control how the fluid auto scroll feels export const config = { // percentage distance from edge of container: startFrom: 0.18, maxSpeedAt: 0.05, // pixels per frame maxScrollSpeed: 25, + // A function used to ease the distance been the startFrom and maxSpeedAt values + // A simple linear function would be: (percentage) => percentage; + // percentage is between 0 and 1 + // result must be between 0 and 1 + ease: (percentage: number) => Math.pow(percentage, 2), }; const origin: Position = { x: 0, y: 0 }; -type Thresholds = {| +type PixelThresholds = {| startFrom: number, maxSpeedAt: number, accelerationPlane: number, |} -const getThresholds = (container: Area, axis: Axis): Thresholds => { +// converts the percentages in the config into actual pixel values +const getPixelThresholds = (container: Area, axis: Axis): PixelThresholds => { const startFrom: number = container[axis.size] * config.startFrom; const maxSpeedAt: number = container[axis.size] * config.maxSpeedAt; const accelerationPlane: number = startFrom - maxSpeedAt; - const thresholds: Thresholds = { + const thresholds: PixelThresholds = { startFrom, maxSpeedAt, accelerationPlane, @@ -55,18 +62,12 @@ const getThresholds = (container: Area, axis: Axis): Thresholds => { return thresholds; }; -const getSpeed = (distance: number, thresholds: Thresholds): number => { +const getSpeed = (distance: number, thresholds: PixelThresholds): number => { // Not close enough to the edge if (distance >= thresholds.startFrom) { return 0; } - // gone past the edge (currently not supported) - // TODO: do not want for window - but need it for droppable? - // if (distance < 0) { - // return 0; - // } - // Already past the maxSpeedAt point if (distance <= thresholds.maxSpeedAt) { @@ -77,7 +78,9 @@ const getSpeed = (distance: number, thresholds: Thresholds): number => { const distancePastStart: number = thresholds.startFrom - distance; const percentage: number = distancePastStart / thresholds.accelerationPlane; - const speed: number = config.maxScrollSpeed * percentage; + const transformed: number = config.ease(percentage); + + const speed: number = config.maxScrollSpeed * transformed; return speed; }; @@ -102,7 +105,7 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { // Negative values to not continue to increase the speed const y: number = (() => { - const thresholds: Thresholds = getThresholds(container, vertical); + const thresholds: PixelThresholds = getPixelThresholds(container, vertical); const isCloserToBottom: boolean = distance.bottom < distance.top; if (isCloserToBottom) { @@ -114,7 +117,7 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { })(); const x: number = (() => { - const thresholds: Thresholds = getThresholds(container, horizontal); + const thresholds: PixelThresholds = getPixelThresholds(container, horizontal); const isCloserToRight: boolean = distance.right < distance.left; if (isCloserToRight) { @@ -186,15 +189,9 @@ export default ({ const requiredFrameScroll: ?Position = getRequiredScroll(closestScrollable.frame, center); - if (!requiredFrameScroll) { - return; + if (requiredFrameScroll && canScrollDroppable(droppable, requiredFrameScroll)) { + scheduleDroppableScroll(droppable.descriptor.id, requiredFrameScroll); } - - if (!canScrollDroppable(droppable, requiredFrameScroll)) { - return; - } - - scheduleDroppableScroll(droppable.descriptor.id, requiredFrameScroll); }; const jumpScroll = (state: State) => { From 6943be059162d12e53b8ce1f549d4c62afd6523a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 30 Jan 2018 20:42:47 +1100 Subject: [PATCH 032/163] updating auto scroll config values --- src/state/auto-scroll-marshal/auto-scroll-marshal.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 41330b0256..a7f5f06086 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -26,12 +26,12 @@ type Args = {| |} // Values used to control how the fluid auto scroll feels -export const config = { +const config = { // percentage distance from edge of container: - startFrom: 0.18, + startFrom: 0.25, maxSpeedAt: 0.05, // pixels per frame - maxScrollSpeed: 25, + maxScrollSpeed: 28, // A function used to ease the distance been the startFrom and maxSpeedAt values // A simple linear function would be: (percentage) => percentage; // percentage is between 0 and 1 From e6385ba46bea3fa4c9d50460af7b37e53cc30d17 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 31 Jan 2018 09:11:41 +1100 Subject: [PATCH 033/163] initial --- .../auto-scroll-marshal.js | 30 +++++- src/state/auto-scroll-marshal/can-scroll.js | 95 ++++++++++++++++--- src/state/reducer.js | 1 + .../drag-drop-context/drag-drop-context.jsx | 4 + 4 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index a7f5f06086..68baee1954 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -1,11 +1,17 @@ // @flow import rafSchd from 'raf-schd'; import getViewport from '../visibility/get-viewport'; -import { isEqual } from '../position'; +import { add, isEqual } from '../position'; import { vertical, horizontal } from '../axis'; import getScrollableDroppableOver from './get-scrollable-droppable-over'; -import { canScrollDroppable, canScrollWindow } from './can-scroll'; +import { + canScrollDroppable, + canScrollWindow, + getWindowOverlap, + getDroppableOverlap, +} from './can-scroll'; import scrollWindow from './scroll-window'; +import getWindowScrollPosition from '../../view/get-window-scroll-position'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; import type { Area, @@ -19,10 +25,12 @@ import type { DraggableLocation, DraggableDimension, ClosestScrollable, + DraggableId, } from '../../types'; type Args = {| scrollDroppable: (id: DroppableId, change: Position) => void, + move: (id: DraggableId, client: Position, windowScroll: Position) => void, |} // Values used to control how the fluid auto scroll feels @@ -138,6 +146,7 @@ const isTooBigForAutoScrolling = (frame: Area, subject: Area): boolean => export default ({ scrollDroppable, + move, }: Args): AutoScrollMarshal => { // TODO: do not scroll if drag has finished const scheduleWindowScroll = rafSchd(scrollWindow); @@ -225,6 +234,15 @@ export default ({ if (canScrollDroppable(droppable, request)) { // not scheduling - jump requests need to be performed instantly + + const overlap: ?Position = getDroppableOverlap(droppable, request); + + if (overlap) { + console.warn('DROPPABLE OVERLAP', overlap); + const client: Position = add(drag.current.client.selection, overlap); + move(drag.initial.descriptor.id, client, getWindowScrollPosition()); + } + scrollDroppable(droppable.descriptor.id, request); return; } @@ -242,6 +260,14 @@ export default ({ console.warn('Jump scroll requested but it cannot be done by Droppable or the Window'); } + const overlap: ?Position = getWindowOverlap(request); + + if (overlap) { + console.warn('WINDOW OVERLAP', overlap); + const client: Position = add(drag.current.client.selection, overlap); + move(drag.initial.descriptor.id, client, getWindowScrollPosition()); + } + // not scheduling - jump requests need to be performed instantly scrollWindow(request); }; diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll-marshal/can-scroll.js index 9d4bab153f..c1cc80169c 100644 --- a/src/state/auto-scroll-marshal/can-scroll.js +++ b/src/state/auto-scroll-marshal/can-scroll.js @@ -15,7 +15,7 @@ import type { type CanScrollArgs = {| max: Position, current: Position, - proposed: Position, + change: Position, |} const origin: Position = { x: 0, y: 0 }; @@ -30,12 +30,12 @@ const getSmallestSignedValue = (value: number) => { const canScroll = ({ max, current, - proposed, + change, }: CanScrollArgs): boolean => { // Only need to be able to move the smallest amount in the desired direction const smallestChange: Position = { - x: getSmallestSignedValue(proposed.x), - y: getSmallestSignedValue(proposed.y), + x: getSmallestSignedValue(change.x), + y: getSmallestSignedValue(change.y), }; const target: Position = add(current, smallestChange); @@ -57,15 +57,14 @@ const canScroll = ({ return true; }; -export const canScrollWindow = (change: Position): boolean => { +const getMaxWindowScroll = (): Position => { const el: ?HTMLElement = document.documentElement; if (!el) { console.error('Cannot find document element'); - return false; + return origin; } - const current: Position = getWindowScrollPosition(); const viewport: Area = getViewport(); const maxScroll: Position = getMaxScroll({ @@ -75,12 +74,17 @@ export const canScrollWindow = (change: Position): boolean => { height: viewport.height, }); - console.log('can scroll window?'); + return maxScroll; +}; + +export const canScrollWindow = (change: Position): boolean => { + const maxScroll: Position = getMaxWindowScroll(); + const currentScroll: Position = getWindowScrollPosition(); return canScroll({ - current, + current: currentScroll, max: maxScroll, - proposed: change, + change, }); }; @@ -94,11 +98,76 @@ export const canScrollDroppable = ( return false; } - console.log('can scroll droppable?'); - return canScroll({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, - proposed: change, + change, + }); +}; + +type GetOverlapArgs = {| + current: Position, + max: Position, + change: Position, +|} + +const getOverlap = ({ + current, + max, + change, +}: GetOverlapArgs): ?Position => { + const target: Position = add(current, change); + + // too far back + if (target.y <= 0 && target.x <= 0) { + console.log('forward overlap'); + const overlap: Position = { + x: target.x, + y: target.y, + }; + return overlap; + } + + // too far forward + if (target.y >= max.y && target.x >= max.x) { + console.log('backward overlap'); + const overlap: Position = subtract(target, max); + return overlap; + } + + // no overlap + return null; +}; + +export const getWindowOverlap = (change: Position): ?Position => { + if (!canScrollWindow(change)) { + return null; + } + + const max: Position = getMaxWindowScroll(); + const current: Position = getWindowScrollPosition(); + + return getOverlap({ + current, + max, + change, + }); +}; + +export const getDroppableOverlap = (droppable: DroppableDimension, change: Position): ?Position => { + if (!canScrollDroppable(droppable, change)) { + return null; + } + + const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; + // Cannot scroll when there is no scroll container! + if (!closestScrollable) { + return null; + } + + return getOverlap({ + current: closestScrollable.scroll.current, + max: closestScrollable.scroll.max, + change, }); }; diff --git a/src/state/reducer.js b/src/state/reducer.js index 1407defe36..442f78fc8b 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -403,6 +403,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { if (action.type === 'MOVE') { const { client, windowScroll } = action.payload; + console.log('moving by', client); return move({ state, clientSelection: client, diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 5f1f42aaff..4ddc417f0b 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -31,6 +31,7 @@ import { } from '../context-keys'; import { clean, + move, publishDraggableDimensions, publishDroppableDimensions, updateDroppableDimensionScroll, @@ -113,6 +114,9 @@ export default class DragDropContext extends React.Component { this.dimensionMarshal = createDimensionMarshal(callbacks); this.scrollMarshal = createAutoScroll({ scrollDroppable: this.dimensionMarshal.scrollDroppable, + move: (id: DraggableId, client: Position, windowScroll: Position) => { + this.store.dispatch(move(id, client, windowScroll)); + }, }); let previous: State = this.store.getState(); From 6c8888ac7d10fe8fd226cfd4368b13895e6f836c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 31 Jan 2018 09:28:13 +1100 Subject: [PATCH 034/163] its alive --- src/state/action-creators.js | 9 +++++++-- .../auto-scroll-marshal/auto-scroll-marshal.js | 17 ++++++++++------- src/state/reducer.js | 4 ++-- .../drag-drop-context/drag-drop-context.jsx | 4 ++-- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 8863de3298..d073e0a0ae 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -143,17 +143,22 @@ export type MoveAction = {| id: DraggableId, client: Position, windowScroll: Position, + shouldAnimate: boolean, |} |} -export const move = (id: DraggableId, +export const move = ( + id: DraggableId, client: Position, - windowScroll: Position): MoveAction => ({ + windowScroll: Position, + shouldAnimate?: boolean = false, +): MoveAction => ({ type: 'MOVE', payload: { id, client, windowScroll, + shouldAnimate, }, }); diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 68baee1954..473c7bc730 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -30,7 +30,7 @@ import type { type Args = {| scrollDroppable: (id: DroppableId, change: Position) => void, - move: (id: DraggableId, client: Position, windowScroll: Position) => void, + move: (id: DraggableId, client: Position, windowScroll: Position, shouldAnimate: boolean) => void, |} // Values used to control how the fluid auto scroll feels @@ -235,12 +235,15 @@ export default ({ if (canScrollDroppable(droppable, request)) { // not scheduling - jump requests need to be performed instantly - const overlap: ?Position = getDroppableOverlap(droppable, request); + // if the window can also not be scrolled - adjust the item + if (!canScrollWindow(request)) { + const overlap: ?Position = getDroppableOverlap(droppable, request); - if (overlap) { - console.warn('DROPPABLE OVERLAP', overlap); - const client: Position = add(drag.current.client.selection, overlap); - move(drag.initial.descriptor.id, client, getWindowScrollPosition()); + if (overlap) { + console.warn('DROPPABLE OVERLAP', overlap); + const client: Position = add(drag.current.client.selection, overlap); + move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); + } } scrollDroppable(droppable.descriptor.id, request); @@ -265,7 +268,7 @@ export default ({ if (overlap) { console.warn('WINDOW OVERLAP', overlap); const client: Position = add(drag.current.client.selection, overlap); - move(drag.initial.descriptor.id, client, getWindowScrollPosition()); + move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); } // not scheduling - jump requests need to be performed instantly diff --git a/src/state/reducer.js b/src/state/reducer.js index 442f78fc8b..451c995879 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -402,13 +402,13 @@ export default (state: State = clean('IDLE'), action: Action): State => { } if (action.type === 'MOVE') { - const { client, windowScroll } = action.payload; + const { client, windowScroll, shouldAnimate } = action.payload; console.log('moving by', client); return move({ state, clientSelection: client, windowScroll, - shouldAnimate: false, + shouldAnimate, }); } diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 4ddc417f0b..2d652a82cc 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -114,8 +114,8 @@ export default class DragDropContext extends React.Component { this.dimensionMarshal = createDimensionMarshal(callbacks); this.scrollMarshal = createAutoScroll({ scrollDroppable: this.dimensionMarshal.scrollDroppable, - move: (id: DraggableId, client: Position, windowScroll: Position) => { - this.store.dispatch(move(id, client, windowScroll)); + move: (id: DraggableId, client: Position, windowScroll: Position, shouldAnimate: boolean) => { + this.store.dispatch(move(id, client, windowScroll, shouldAnimate)); }, }); From da03a2b6050b84a642899c9a455b510e60f66451 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 31 Jan 2018 11:17:10 +1100 Subject: [PATCH 035/163] foreign list is almost working --- .../get-best-cross-axis-droppable.js | 2 +- .../move-cross-axis/get-closest-draggable.js | 13 +++-- .../move-to-new-droppable/to-foreign-list.js | 10 +++- .../move-to-new-droppable/to-home-list.js | 5 +- ...et-result.js => get-scroll-jump-result.js} | 46 +++++---------- .../move-to-next-index/in-foreign-list.js | 57 ++++++++----------- src/state/move-to-next-index/in-home-list.js | 42 +++++--------- src/state/reducer.js | 1 - src/state/with-droppable-displacement.js | 18 ++++++ src/state/with-droppable-scroll.js | 16 ++++++ .../sensor/create-keyboard-sensor.js | 13 ++++- src/view/key-codes.js | 4 ++ 12 files changed, 123 insertions(+), 104 deletions(-) rename src/state/move-to-next-index/{get-result.js => get-scroll-jump-result.js} (54%) create mode 100644 src/state/with-droppable-displacement.js create mode 100644 src/state/with-droppable-scroll.js diff --git a/src/state/move-cross-axis/get-best-cross-axis-droppable.js b/src/state/move-cross-axis/get-best-cross-axis-droppable.js index 49ebed3d67..868abefdc5 100644 --- a/src/state/move-cross-axis/get-best-cross-axis-droppable.js +++ b/src/state/move-cross-axis/get-best-cross-axis-droppable.js @@ -61,7 +61,7 @@ export default ({ // Remove any droppables that are not partially visible .filter((droppable: DroppableDimension): boolean => { const clipped: ?Area = droppable.viewport.clipped; - // subject is not visible + // subject is not visible at all in frame if (!clipped) { return false; } diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index b328a0eb87..404faaea1d 100644 --- a/src/state/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-cross-axis/get-closest-draggable.js @@ -1,5 +1,5 @@ // @flow -import { distance } from '../position'; +import { add, distance } from '../position'; import getViewport from '../visibility/get-viewport'; import { isTotallyVisible } from '../visibility/is-visible'; import type { @@ -19,6 +19,8 @@ type Args = {| insideDestination: DraggableDimension[], |} +const origin: Position = { x: 0, y: 0 }; + export default ({ axis, pageCenter, @@ -31,6 +33,9 @@ export default ({ } const viewport: Area = getViewport(); + const scrollDisplacement: Position = destination.viewport.closestScrollable ? + destination.viewport.closestScrollable.scroll.diff.displacement : + origin; const result: DraggableDimension[] = insideDestination // Remove any options that are hidden by overflow @@ -42,9 +47,9 @@ export default ({ viewport, })) .sort((a: DraggableDimension, b: DraggableDimension): number => { - // TODO: need to consider droppable scroll - const distanceToA = distance(pageCenter, a.page.withMargin.center); - const distanceToB = distance(pageCenter, b.page.withMargin.center); + // Need to consider the change in scroll in the destination + const distanceToA = distance(pageCenter, add(a.page.withMargin.center, scrollDisplacement)); + const distanceToB = distance(pageCenter, add(b.page.withMargin.center, scrollDisplacement)); // if a is closer - return a if (distanceToA < distanceToB) { diff --git a/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js index bb0fcd027f..c21ed9251f 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js +++ b/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js @@ -3,6 +3,7 @@ import moveToEdge from '../../move-to-edge'; import type { Result } from '../move-cross-axis-types'; import getDisplacement from '../../get-displacement'; import getViewport from '../../visibility/get-viewport'; +import { add } from '../../position'; import type { Axis, Position, @@ -13,6 +14,8 @@ import type { Displacement, } from '../../../types'; +const origin: Position = { x: 0, y: 0 }; + type Args = {| amount: Position, pageCenter: Position, @@ -115,8 +118,13 @@ export default ({ }, }; + const scrollDisplacement: Position = droppable.viewport.closestScrollable ? + droppable.viewport.closestScrollable.scroll.diff.displacement : + origin; + const withDisplacement: Position = add(newCenter, scrollDisplacement); + return { - pageCenter: newCenter, + pageCenter: withDisplacement, impact: newImpact, }; }; diff --git a/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js index 590bbe7b89..255c7619fc 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js +++ b/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js @@ -2,6 +2,7 @@ import moveToEdge from '../../move-to-edge'; import getViewport from '../../visibility/get-viewport'; import getDisplacement from '../../get-displacement'; +import withDroppableScroll from '../../with-droppable-scroll'; import type { Edge } from '../../move-to-edge'; import type { Result } from '../move-cross-axis-types'; import type { @@ -24,6 +25,8 @@ type Args = {| previousImpact: DragImpact, |} +const origin: Position = { x: 0, y: 0 }; + export default ({ amount, originalIndex, @@ -128,7 +131,7 @@ export default ({ }; return { - pageCenter: newCenter, + pageCenter: withDroppableScroll(droppable, newCenter), impact: newImpact, }; }; diff --git a/src/state/move-to-next-index/get-result.js b/src/state/move-to-next-index/get-scroll-jump-result.js similarity index 54% rename from src/state/move-to-next-index/get-result.js rename to src/state/move-to-next-index/get-scroll-jump-result.js index b898a277ad..b01c4153c6 100644 --- a/src/state/move-to-next-index/get-result.js +++ b/src/state/move-to-next-index/get-scroll-jump-result.js @@ -1,56 +1,35 @@ // @flow import { subtract } from '../position'; -import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; -import getViewport from '../visibility/get-viewport'; import type { Result } from './move-to-next-index-types'; import type { - DroppableDimension, - DraggableDimension, Position, + DroppableDimension, DragImpact, } from '../../types'; type Args = {| - destination: DroppableDimension, - draggable: DraggableDimension, - previousPageCenter: Position, newPageCenter: Position, - newImpact: DragImpact, + previousPageCenter: Position, + droppable: DroppableDimension, + previousImpact: DragImpact, |} const origin: Position = { x: 0, y: 0 }; export default ({ - destination, - draggable, - previousPageCenter, newPageCenter, - newImpact, + previousPageCenter, + droppable, + previousImpact, }: Args): Result => { - const isTotallyVisible: boolean = isTotallyVisibleInNewLocation({ - draggable, - destination, - newPageCenter, - viewport: getViewport(), - }); - const scrollDiff: Position = destination.viewport.closestScrollable ? - destination.viewport.closestScrollable.scroll.diff.value : - origin; - - if (isTotallyVisible) { - const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); - - return { - pageCenter: withScrollDiff, - impact: newImpact, - scrollJumpRequest: null, - }; - } - // The full distance required to get from the previous page center to the new page center const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); // We need to consider how much the droppable scroll has changed + const scrollDiff: Position = droppable.viewport.closestScrollable ? + droppable.viewport.closestScrollable.scroll.diff.value : + origin; + // The actual scroll required to move into the next place const requiredScroll: Position = subtract(requiredDistance, scrollDiff); @@ -58,7 +37,8 @@ export default ({ // Using the previous page center with a new impact // as we are not visually moving the Draggable pageCenter: previousPageCenter, - impact: newImpact, + impact: previousImpact, scrollJumpRequest: requiredScroll, }; }; + diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index 12982ecf7d..fe0d494446 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -1,11 +1,12 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import { patch } from '../position'; +import withDroppableScroll from '../with-droppable-scroll'; import moveToEdge from '../move-to-edge'; import getDisplacement from '../get-displacement'; import getViewport from '../visibility/get-viewport'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; -import getResult from './get-result'; +import getScrollJumpResult from './get-scroll-jump-result'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import type { @@ -18,6 +19,8 @@ import type { Area, } from '../../types'; +const origin: Position = { x: 0, y: 0 }; + export default ({ isMovingForward, draggableId, @@ -83,27 +86,21 @@ export default ({ destinationAxis: droppable.axis, }); - // const isVisible: boolean = (() => { - // // Moving into placeholder position - // // Usually this would be outside of the visible bounds - // if (isMovingPastLastIndex) { - // return true; - // } - - // // checking the shifted draggable rather than just the new center - // // as the new center might not be visible but the whole draggable - // // might be partially visible - // return isTotallyVisibleInNewLocation({ - // draggable, - // destination: droppable, - // newCenter, - // viewport, - // }); - // })(); - - // if (!isVisible) { - // return null; - // } + const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ + draggable, + destination: droppable, + newPageCenter, + viewport, + }); + + if (!isVisibleInNewLocation) { + return getScrollJumpResult({ + newPageCenter, + previousPageCenter, + droppable, + previousImpact, + }); + } // at this point we know that the destination is droppable const movingRelativeToDisplacement: Displacement = { @@ -155,15 +152,9 @@ export default ({ direction: droppable.axis.direction, }; - if (isMovingPastLastIndex) { - // TODO! - } - - return getResult({ - draggable, - destination: droppable, - previousPageCenter, - newPageCenter, - newImpact, - }); + return { + pageCenter: withDroppableScroll(droppable, newPageCenter), + impact: newImpact, + scrollJumpRequest: null, + }; }; diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 65ecb944f8..939e57d573 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,8 +1,10 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { subtract, patch } from '../position'; +import { patch } from '../position'; +import withDroppableScroll from '../with-droppable-scroll'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import getViewport from '../visibility/get-viewport'; +import getScrollJumpResult from './get-scroll-jump-result'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; @@ -17,8 +19,6 @@ import type { Area, } from '../../types'; -const origin: Position = { x: 0, y: 0 }; - export default ({ isMovingForward, draggableId, @@ -61,6 +61,7 @@ export default ({ return null; } + const viewport: Area = getViewport(); const destination: DraggableDimension = insideDroppable[proposedIndex]; const isMovingTowardStart = (isMovingForward && proposedIndex <= startIndex) || (!isMovingForward && proposedIndex >= startIndex); @@ -86,28 +87,16 @@ export default ({ draggable, destination: droppable, newPageCenter, - viewport: getViewport(), + viewport, }); if (!isVisibleInNewLocation) { - // The full distance required to get from the previous page center to the new page center - const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); - - // We need to consider how much the droppable scroll has changed - const scrollDiff: Position = droppable.viewport.closestScrollable ? - droppable.viewport.closestScrollable.scroll.diff.value : - origin; - - // The actual scroll required to move into the next place - const requiredScroll: Position = subtract(requiredDistance, scrollDiff); - - return { - // Using the previous page center with a new impact - // as we are not visually moving the Draggable - pageCenter: previousPageCenter, - impact: previousImpact, - scrollJumpRequest: requiredScroll, - }; + return getScrollJumpResult({ + newPageCenter, + previousPageCenter, + droppable, + previousImpact, + }); } // Calculate DragImpact @@ -125,7 +114,7 @@ export default ({ [destinationDisplacement, ...previousImpact.movement.displaced]); // update impact with visibility - stops redundant work! - const viewport: Area = getViewport(); + const displaced: Displacement[] = modified .map((displacement: Displacement): Displacement => { const target: DraggableDimension = draggables[displacement.draggableId]; @@ -155,13 +144,8 @@ export default ({ direction: droppable.axis.direction, }; - const scrollDiff: Position = droppable.viewport.closestScrollable ? - droppable.viewport.closestScrollable.scroll.diff.value : - origin; - const withScrollDiff: Position = subtract(newPageCenter, scrollDiff); - return { - pageCenter: withScrollDiff, + pageCenter: withDroppableScroll(droppable, newPageCenter), impact: newImpact, scrollJumpRequest: null, }; diff --git a/src/state/reducer.js b/src/state/reducer.js index 451c995879..56cf3181b4 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -403,7 +403,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { if (action.type === 'MOVE') { const { client, windowScroll, shouldAnimate } = action.payload; - console.log('moving by', client); return move({ state, clientSelection: client, diff --git a/src/state/with-droppable-displacement.js b/src/state/with-droppable-displacement.js new file mode 100644 index 0000000000..ee3373a8e1 --- /dev/null +++ b/src/state/with-droppable-displacement.js @@ -0,0 +1,18 @@ +// @flow +import { add } from './position'; +import type { + Position, + ClosestScrollable, + DroppableDimension, +} from '../types'; + +const origin: Position = { x: 0, y: 0 }; + +export default (droppable: DroppableDimension, point: Position): Position => { + const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; + if (!closestScrollable) { + return origin; + } + + return add(point, closestScrollable.scroll.diff.displacement); +}; diff --git a/src/state/with-droppable-scroll.js b/src/state/with-droppable-scroll.js new file mode 100644 index 0000000000..e17734c2bf --- /dev/null +++ b/src/state/with-droppable-scroll.js @@ -0,0 +1,16 @@ +// @flow +import { subtract } from './position'; +import type { + Position, + ClosestScrollable, + DroppableDimension, +} from '../types'; + +export default (droppable: DroppableDimension, point: Position): Position => { + const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; + if (!closestScrollable) { + return point; + } + + return subtract(point, closestScrollable.scroll.diff.value); +}; diff --git a/src/view/drag-handle/sensor/create-keyboard-sensor.js b/src/view/drag-handle/sensor/create-keyboard-sensor.js index 29ce9f95dd..0e1d85eb91 100644 --- a/src/view/drag-handle/sensor/create-keyboard-sensor.js +++ b/src/view/drag-handle/sensor/create-keyboard-sensor.js @@ -23,6 +23,10 @@ type ExecuteBasedOnDirection = {| const noop = () => { }; +const scrollJumpKeys: number[] = [ + keyCodes.pageDown, keyCodes.pageUp, keyCodes.home, keyCodes.end, +]; + export default ({ callbacks, getDraggableRef, @@ -158,14 +162,21 @@ export default ({ } blockStandardKeyEvents(event); + + // blocking scroll jumping at this time + if (scrollJumpKeys.indexOf(event.keyCode) >= 0) { + stopEvent(event); + } }; const windowBindings = { // any mouse actions kills a drag mousedown: cancel, mouseup: cancel, + click: cancel, + // resizing the browser kills a drag resize: cancel, - // Cancel if the user is using the mouse wheel + // kill if the user is using the mouse wheel // We are not supporting wheel / trackpad scrolling with keyboard dragging wheel: cancel, // Need to respond instantly to a jump scroll request diff --git a/src/view/key-codes.js b/src/view/key-codes.js index 0c8939afba..f8d433091e 100644 --- a/src/view/key-codes.js +++ b/src/view/key-codes.js @@ -3,6 +3,10 @@ export const tab: number = 9; export const enter: number = 13; export const escape: number = 27; export const space: number = 32; +export const pageUp: number = 33; +export const pageDown: number = 34; +export const end: number = 35; +export const home: number = 36; export const arrowLeft: number = 37; export const arrowUp: number = 38; export const arrowRight: number = 39; From 75461e9d2f7b2601387dff0b380434bea26b09f4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 31 Jan 2018 11:34:11 +1100 Subject: [PATCH 036/163] using a map for keyblocking --- .../sensor/create-keyboard-sensor.js | 17 ++++++++++++----- .../util/block-standard-key-events.js | 14 +++++++++----- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/view/drag-handle/sensor/create-keyboard-sensor.js b/src/view/drag-handle/sensor/create-keyboard-sensor.js index 0e1d85eb91..40aa90c8b9 100644 --- a/src/view/drag-handle/sensor/create-keyboard-sensor.js +++ b/src/view/drag-handle/sensor/create-keyboard-sensor.js @@ -21,11 +21,18 @@ type ExecuteBasedOnDirection = {| horizontal: () => void, |} -const noop = () => { }; +type KeyMap = { + [key: number]: true +} + +const scrollJumpKeys: KeyMap = { + [keyCodes.pageDown]: true, + [keyCodes.pageUp]: true, + [keyCodes.home]: true, + [keyCodes.end]: true, +}; -const scrollJumpKeys: number[] = [ - keyCodes.pageDown, keyCodes.pageUp, keyCodes.home, keyCodes.end, -]; +const noop = () => { }; export default ({ callbacks, @@ -164,7 +171,7 @@ export default ({ blockStandardKeyEvents(event); // blocking scroll jumping at this time - if (scrollJumpKeys.indexOf(event.keyCode) >= 0) { + if (scrollJumpKeys[event.keyCode]) { stopEvent(event); } }; diff --git a/src/view/drag-handle/util/block-standard-key-events.js b/src/view/drag-handle/util/block-standard-key-events.js index 3a253b6813..e43b220c82 100644 --- a/src/view/drag-handle/util/block-standard-key-events.js +++ b/src/view/drag-handle/util/block-standard-key-events.js @@ -2,15 +2,19 @@ import * as keyCodes from '../../key-codes'; import stopEvent from './stop-event'; -const blocked: number[] = [ +type KeyMap = { + [key: number]: true +} + +const blocked: KeyMap = { // submission - keyCodes.enter, + [keyCodes.enter]: true, // tabbing - keyCodes.tab, -]; + [keyCodes.tab]: true, +}; export default (event: KeyboardEvent) => { - if (blocked.indexOf(event.keyCode) >= 0) { + if (blocked[event.keyCode]) { stopEvent(event); } }; From e156fbbae1f1c5e8ba5d73262cb5371d93348531 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 31 Jan 2018 14:53:50 +1100 Subject: [PATCH 037/163] adding cancel for keyboard on touchstart --- src/view/drag-handle/sensor/create-keyboard-sensor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/drag-handle/sensor/create-keyboard-sensor.js b/src/view/drag-handle/sensor/create-keyboard-sensor.js index 40aa90c8b9..36c038799c 100644 --- a/src/view/drag-handle/sensor/create-keyboard-sensor.js +++ b/src/view/drag-handle/sensor/create-keyboard-sensor.js @@ -181,6 +181,7 @@ export default ({ mousedown: cancel, mouseup: cancel, click: cancel, + touchstart: cancel, // resizing the browser kills a drag resize: cancel, // kill if the user is using the mouse wheel From 2cf922850ec421695f1a43dae56538c70b33f21a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 31 Jan 2018 22:08:52 +1100 Subject: [PATCH 038/163] minor cleanup --- .../auto-scroll-marshal/auto-scroll-marshal.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 473c7bc730..2bb15c7ea2 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -203,6 +203,16 @@ export default ({ } }; + const performMove = (state: State, offset: Position) => { + const drag: ?DragState = state.drag; + if (!drag) { + return; + } + + const client: Position = add(drag.current.client.selection, offset); + move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); + }; + const jumpScroll = (state: State) => { const drag: ?DragState = state.drag; @@ -241,8 +251,7 @@ export default ({ if (overlap) { console.warn('DROPPABLE OVERLAP', overlap); - const client: Position = add(drag.current.client.selection, overlap); - move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); + performMove(state, overlap); } } @@ -267,8 +276,7 @@ export default ({ if (overlap) { console.warn('WINDOW OVERLAP', overlap); - const client: Position = add(drag.current.client.selection, overlap); - move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); + performMove(state, overlap); } // not scheduling - jump requests need to be performed instantly From 2b746a2cb7cea24c7ffd19391e883c52a1022d34 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 31 Jan 2018 22:28:29 +1100 Subject: [PATCH 039/163] wip --- .../auto-scroll-marshal/auto-scroll-marshal.js | 2 ++ src/state/auto-scroll-marshal/can-scroll.js | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 2bb15c7ea2..c728b0aef3 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -270,6 +270,8 @@ export default ({ if (!canScrollWindow(request)) { console.warn('Jump scroll requested but it cannot be done by Droppable or the Window'); + performMove(state, request); + return; } const overlap: ?Position = getWindowOverlap(request); diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll-marshal/can-scroll.js index c1cc80169c..94cc1e9162 100644 --- a/src/state/auto-scroll-marshal/can-scroll.js +++ b/src/state/auto-scroll-marshal/can-scroll.js @@ -44,13 +44,17 @@ const canScroll = ({ return false; } + console.log('smallest change', smallestChange); + // Too far back - if (target.y <= 0 && target.x <= 0) { + if (target.y < 0 || target.x < 0) { + console.log('too far back'); return false; } // Too far forward - if (target.y >= max.y && target.x >= max.x) { + if (target.y > max.y || target.x > max.x) { + console.log('too far forward'); return false; } @@ -81,6 +85,11 @@ export const canScrollWindow = (change: Position): boolean => { const maxScroll: Position = getMaxWindowScroll(); const currentScroll: Position = getWindowScrollPosition(); + console.group('can scroll window'); + console.log('max scroll', maxScroll); + console.log('current', currentScroll); + console.groupEnd(); + return canScroll({ current: currentScroll, max: maxScroll, From 83c709bceb01e4a1cb1354574b4b2748950ac090 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 1 Feb 2018 10:49:29 +1100 Subject: [PATCH 040/163] progress --- .../auto-scroll-marshal.js | 2 ++ src/state/auto-scroll-marshal/can-scroll.js | 22 +++++++++++-------- src/state/dimension.js | 2 ++ src/state/get-max-scroll.js | 10 ++++----- .../droppable-dimension-publisher.jsx | 17 +++++++++++--- stories/3-board-story.js | 3 ++- 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index c728b0aef3..8d4fc1e9d4 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -239,6 +239,7 @@ export default ({ if (closestScrollable) { if (isTooBigForAutoScrolling(closestScrollable.frame, draggable.page.withMargin)) { + performMove(state, request); return; } @@ -265,6 +266,7 @@ export default ({ // Scroll the window if we can if (isTooBigForAutoScrolling(getViewport(), draggable.page.withMargin)) { + performMove(state, request); return; } diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll-marshal/can-scroll.js index 94cc1e9162..1714e12454 100644 --- a/src/state/auto-scroll-marshal/can-scroll.js +++ b/src/state/auto-scroll-marshal/can-scroll.js @@ -27,6 +27,12 @@ const getSmallestSignedValue = (value: number) => { return value > 0 ? 1 : -1; }; +const isTooFarBack = (targetScroll: Position): boolean => + targetScroll.y < 0 || targetScroll.x < 0; + +const isTooFarForward = (targetScroll: Position, maxScroll: Position): boolean => + targetScroll.y > maxScroll.y || targetScroll.x > maxScroll.x; + const canScroll = ({ max, current, @@ -46,14 +52,12 @@ const canScroll = ({ console.log('smallest change', smallestChange); - // Too far back - if (target.y < 0 || target.x < 0) { + if (isTooFarBack(target)) { console.log('too far back'); return false; } - // Too far forward - if (target.y > max.y || target.x > max.x) { + if (isTooFarForward(target, max)) { console.log('too far forward'); return false; } @@ -71,6 +75,9 @@ const getMaxWindowScroll = (): Position => { const viewport: Area = getViewport(); + // window.innerWidth / innerHeight includes scrollbar + // however the scrollHeight / scrollWidth do not :( + const maxScroll: Position = getMaxScroll({ scrollHeight: el.scrollHeight, scrollWidth: el.scrollWidth, @@ -127,9 +134,7 @@ const getOverlap = ({ }: GetOverlapArgs): ?Position => { const target: Position = add(current, change); - // too far back - if (target.y <= 0 && target.x <= 0) { - console.log('forward overlap'); + if (isTooFarBack(target)) { const overlap: Position = { x: target.x, y: target.y, @@ -137,8 +142,7 @@ const getOverlap = ({ return overlap; } - // too far forward - if (target.y >= max.y && target.x >= max.x) { + if (isTooFarForward(target, max)) { console.log('backward overlap'); const overlap: Position = subtract(target, max); return overlap; diff --git a/src/state/dimension.js b/src/state/dimension.js index 9ec914f900..6a7a89cde8 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -186,6 +186,8 @@ export const getDroppableDimension = ({ width: frame.width, }); + console.warn('DROPPABLE MAX', maxScroll); + const result: ClosestScrollable = { frame, shouldClipSubject: closest.shouldClipSubject, diff --git a/src/state/get-max-scroll.js b/src/state/get-max-scroll.js index fcdd4004fe..8a945cf7e2 100644 --- a/src/state/get-max-scroll.js +++ b/src/state/get-max-scroll.js @@ -23,11 +23,11 @@ export default ({ // Due to scroll bars sometimes the width / height can be greater // than the scrollWidth / scrollHeight - const adjustedMaxScroll: Position = { - x: Math.max(0, maxScroll.x), - y: Math.max(0, maxScroll.y), - }; + // const adjustedMaxScroll: Position = { + // x: Math.max(0, maxScroll.x), + // y: Math.max(0, maxScroll.y), + // }; - return adjustedMaxScroll; + return maxScroll; }; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index a409c5879f..4963fae2ac 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -269,18 +269,29 @@ export default class DroppableDimensionPublisher extends Component { return null; } + console.log('closest', closestScrollable); + // TODO: add margin to client? const frameClient: Area = getArea(closestScrollable.getBoundingClientRect()); const scrollWidth: number = closestScrollable.scrollWidth; const scrollHeight: number = closestScrollable.scrollHeight; const scroll: Position = this.getClosestScroll(); + const verticalScrollBarWidth: number = closestScrollable.offsetWidth - closestScrollable.clientWidth; + const horizontalScrollBarHeight: number = closestScrollable.offsetHeight - closestScrollable.clientHeight; + + const scrollWidthWithScrollBar: number = scrollWidth + verticalScrollBarWidth; + const scrollHeightWithScrollBar: number = scrollHeight + horizontalScrollBarHeight; + + console.log('vertical scroll bar width', verticalScrollBarWidth); + console.log('horizontal scroll bar height', horizontalScrollBarHeight); + return { frameClient, - scrollWidth, - scrollHeight, + scrollWidth: scrollWidthWithScrollBar, + scrollHeight: scrollHeightWithScrollBar, scroll, - shouldClipSubject: !this.props.ignoreContainerClipping, + shouldClipSubject: !ignoreContainerClipping, }; })(); diff --git a/stories/3-board-story.js b/stories/3-board-story.js index 4d0358a2fe..2377d2090c 100644 --- a/stories/3-board-story.js +++ b/stories/3-board-story.js @@ -13,8 +13,9 @@ storiesOf('board', module) .add('simple', () => ( )) + // TODO: revert to large .add('large data set', () => ( - + )) .add('long lists in a short container', () => ( From d1aa5b7fd1d762739d19f09c1b14a583e4373927 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 1 Feb 2018 15:59:18 +1100 Subject: [PATCH 041/163] progress? --- src/state/auto-scroll-marshal/can-scroll.js | 34 ++++++++++++------- src/state/get-scroll-safe-area-for-element.js | 19 +++++++++++ src/state/visibility/get-viewport.js | 19 ++--------- .../droppable-dimension-publisher.jsx | 20 +++-------- src/view/get-window-scroll-position.js | 19 ++++++++--- 5 files changed, 63 insertions(+), 48 deletions(-) create mode 100644 src/state/get-scroll-safe-area-for-element.js diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll-marshal/can-scroll.js index 1714e12454..382701adab 100644 --- a/src/state/auto-scroll-marshal/can-scroll.js +++ b/src/state/auto-scroll-marshal/can-scroll.js @@ -27,10 +27,16 @@ const getSmallestSignedValue = (value: number) => { return value > 0 ? 1 : -1; }; -const isTooFarBack = (targetScroll: Position): boolean => +const isTooFarBackInBothDirections = (targetScroll: Position): boolean => + targetScroll.y < 0 && targetScroll.x < 0; + +const isTooFarForwardInBothDirections = (targetScroll: Position, maxScroll: Position): boolean => + targetScroll.y > maxScroll.y && targetScroll.x > maxScroll.x; + +const isTooFarBackInEitherDirection = (targetScroll: Position): boolean => targetScroll.y < 0 || targetScroll.x < 0; -const isTooFarForward = (targetScroll: Position, maxScroll: Position): boolean => +const isTooFarForwardInEitherDirection = (targetScroll: Position, maxScroll: Position): boolean => targetScroll.y > maxScroll.y || targetScroll.x > maxScroll.x; const canScroll = ({ @@ -52,12 +58,12 @@ const canScroll = ({ console.log('smallest change', smallestChange); - if (isTooFarBack(target)) { + if (isTooFarBackInBothDirections(target)) { console.log('too far back'); return false; } - if (isTooFarForward(target, max)) { + if (isTooFarForwardInBothDirections(target, max)) { console.log('too far forward'); return false; } @@ -133,18 +139,20 @@ const getOverlap = ({ change, }: GetOverlapArgs): ?Position => { const target: Position = add(current, change); + console.log('getting overlap'); - if (isTooFarBack(target)) { - const overlap: Position = { - x: target.x, - y: target.y, - }; - return overlap; + if (isTooFarBackInEitherDirection(target)) { + console.log('backward overlap'); + return target; } - if (isTooFarForward(target, max)) { - console.log('backward overlap'); - const overlap: Position = subtract(target, max); + if (isTooFarForwardInEitherDirection(target, max)) { + const trimmedMax: Position = { + x: target.x === 0 ? 0 : max.x, + y: target.y === 0 ? 0 : max.y, + }; + const overlap: Position = subtract(target, trimmedMax); + console.log('forward overlap', target, overlap); return overlap; } diff --git a/src/state/get-scroll-safe-area-for-element.js b/src/state/get-scroll-safe-area-for-element.js new file mode 100644 index 0000000000..f191dd8fb9 --- /dev/null +++ b/src/state/get-scroll-safe-area-for-element.js @@ -0,0 +1,19 @@ +// @flow +import type { Area } from '../types'; +import getArea from './get-area'; + +export default (el: HTMLElement): Area => { + const top: number = el.scrollTop; + const left: number = el.scrollLeft; + const width: number = el.clientWidth; + const height: number = el.clientHeight; + + // computed + const right: number = left + width; + const bottom: number = top + height; + + return getArea({ + top, left, right, bottom, + }); +}; + diff --git a/src/state/visibility/get-viewport.js b/src/state/visibility/get-viewport.js index 3639c6d260..c11e3c37aa 100644 --- a/src/state/visibility/get-viewport.js +++ b/src/state/visibility/get-viewport.js @@ -1,19 +1,6 @@ // @flow import type { Area } from '../../types'; -import getArea from '../get-area'; +import getScrollSafeAreaForElement from '../get-scroll-safe-area-for-element'; -export default (): Area => { - // would use window.scrollY and window.scrollX but it is not supported in ie11 - const top: number = window.pageYOffset; - const left: number = window.pageXOffset; - const width: number = window.innerWidth; - const height: number = window.innerHeight; - - // computed - const right: number = left + width; - const bottom: number = top + height; - - return getArea({ - top, left, right, bottom, - }); -}; +export default (): Area => + getScrollSafeAreaForElement((document.documentElement: any)); diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 4963fae2ac..678b5921a9 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -9,6 +9,7 @@ import getArea from '../../state/get-area'; import { getDroppableDimension } from '../../state/dimension'; import getClosestScrollable from '../get-closest-scrollable'; import { dimensionMarshalKey } from '../context-keys'; +import getScrollSafeAreaForElement from '../../state/get-scroll-safe-area-for-element'; import type { DimensionMarshal, DroppableCallbacks, @@ -269,27 +270,16 @@ export default class DroppableDimensionPublisher extends Component { return null; } - console.log('closest', closestScrollable); - // TODO: add margin to client? - const frameClient: Area = getArea(closestScrollable.getBoundingClientRect()); + const frameClient: Area = getScrollSafeAreaForElement((closestScrollable: any)); + const scroll: Position = this.getClosestScroll(); const scrollWidth: number = closestScrollable.scrollWidth; const scrollHeight: number = closestScrollable.scrollHeight; - const scroll: Position = this.getClosestScroll(); - - const verticalScrollBarWidth: number = closestScrollable.offsetWidth - closestScrollable.clientWidth; - const horizontalScrollBarHeight: number = closestScrollable.offsetHeight - closestScrollable.clientHeight; - - const scrollWidthWithScrollBar: number = scrollWidth + verticalScrollBarWidth; - const scrollHeightWithScrollBar: number = scrollHeight + horizontalScrollBarHeight; - - console.log('vertical scroll bar width', verticalScrollBarWidth); - console.log('horizontal scroll bar height', horizontalScrollBarHeight); return { frameClient, - scrollWidth: scrollWidthWithScrollBar, - scrollHeight: scrollHeightWithScrollBar, + scrollWidth, + scrollHeight, scroll, shouldClipSubject: !ignoreContainerClipping, }; diff --git a/src/view/get-window-scroll-position.js b/src/view/get-window-scroll-position.js index 15d0b6d050..5744d3ba88 100644 --- a/src/view/get-window-scroll-position.js +++ b/src/view/get-window-scroll-position.js @@ -1,8 +1,19 @@ // @flow import type { Position } from '../types'; -export default (): Position => ({ - x: window.pageXOffset, - y: window.pageYOffset, -}); +const origin: Position = { x: 0, y: 0 }; + +export default (): Position => { + const el: ?HTMLElement = document.documentElement; + + // should never happen + if (!el) { + return origin; + } + + return { + x: el.scrollLeft, + y: el.scrollTop, + }; +}; From c0ecedb7bcd79fb721c1af21588d53741245db51 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 1 Feb 2018 17:15:43 +1100 Subject: [PATCH 042/163] it is working better --- .../auto-scroll-marshal.js | 1 + src/state/auto-scroll-marshal/can-scroll.js | 40 ++++++++++++++----- src/state/get-max-scroll.js | 14 +++---- src/state/get-scroll-safe-area-for-element.js | 19 --------- src/state/position.js | 7 ++++ src/state/visibility/get-viewport.js | 21 ++++++++-- .../droppable-dimension-publisher.jsx | 21 ++++++++-- 7 files changed, 80 insertions(+), 43 deletions(-) delete mode 100644 src/state/get-scroll-safe-area-for-element.js diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index 8d4fc1e9d4..e4287cb230 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -173,6 +173,7 @@ export default ({ const requiredWindowScroll: ?Position = getRequiredScroll(viewport, center); if (requiredWindowScroll && canScrollWindow(requiredWindowScroll)) { + console.log('scheduling window scroll', requiredWindowScroll); scheduleWindowScroll(requiredWindowScroll); return; } diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll-marshal/can-scroll.js index 382701adab..76067ceb85 100644 --- a/src/state/auto-scroll-marshal/can-scroll.js +++ b/src/state/auto-scroll-marshal/can-scroll.js @@ -1,5 +1,5 @@ // @flow -import { add, isEqual, subtract } from '../position'; +import { add, apply, isEqual, subtract } from '../position'; // TODO: state reaching into VIEW :( import getWindowScrollPosition from '../../view/get-window-scroll-position'; import getViewport from '../visibility/get-viewport'; @@ -27,17 +27,27 @@ const getSmallestSignedValue = (value: number) => { return value > 0 ? 1 : -1; }; -const isTooFarBackInBothDirections = (targetScroll: Position): boolean => - targetScroll.y < 0 && targetScroll.x < 0; +const floor = apply(Math.floor); -const isTooFarForwardInBothDirections = (targetScroll: Position, maxScroll: Position): boolean => - targetScroll.y > maxScroll.y && targetScroll.x > maxScroll.x; +const isTooFarBackInBothDirections = (targetScroll: Position): boolean => { + const floored: Position = floor(targetScroll); + return floored.y <= 0 && floored.x <= 0; +}; -const isTooFarBackInEitherDirection = (targetScroll: Position): boolean => - targetScroll.y < 0 || targetScroll.x < 0; +const isTooFarForwardInBothDirections = (targetScroll: Position, maxScroll: Position): boolean => { + const floored: Position = floor(targetScroll); + return floored.y >= maxScroll.y && floored.x >= maxScroll.x; +}; -const isTooFarForwardInEitherDirection = (targetScroll: Position, maxScroll: Position): boolean => - targetScroll.y > maxScroll.y || targetScroll.x > maxScroll.x; +const isTooFarBackInEitherDirection = (targetScroll: Position): boolean => { + const floored: Position = floor(targetScroll); + return floored.y < 0 || floored.x < 0; +}; + +const isTooFarForwardInEitherDirection = (targetScroll: Position, maxScroll: Position): boolean => { + const floored: Position = floor(targetScroll); + return floored.y > maxScroll.y || floored.x > maxScroll.x; +}; const canScroll = ({ max, @@ -51,6 +61,7 @@ const canScroll = ({ }; const target: Position = add(current, smallestChange); + console.log('floored target', floor(target)); if (isEqual(target, origin)) { return false; @@ -59,12 +70,12 @@ const canScroll = ({ console.log('smallest change', smallestChange); if (isTooFarBackInBothDirections(target)) { - console.log('too far back'); + console.log('too far back', { target }); return false; } if (isTooFarForwardInBothDirections(target, max)) { - console.log('too far forward'); + console.log('too far forward', { target, max }); return false; } @@ -101,6 +112,11 @@ export const canScrollWindow = (change: Position): boolean => { console.group('can scroll window'); console.log('max scroll', maxScroll); console.log('current', currentScroll); + console.log('can scroll?', canScroll({ + current: currentScroll, + max: maxScroll, + change, + })); console.groupEnd(); return canScroll({ @@ -120,6 +136,8 @@ export const canScrollDroppable = ( return false; } + console.warn('can scroll droppable?'); + return canScroll({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, diff --git a/src/state/get-max-scroll.js b/src/state/get-max-scroll.js index 8a945cf7e2..877afdad80 100644 --- a/src/state/get-max-scroll.js +++ b/src/state/get-max-scroll.js @@ -21,13 +21,13 @@ export default ({ { x: width, y: height } ); - // Due to scroll bars sometimes the width / height can be greater - // than the scrollWidth / scrollHeight - // const adjustedMaxScroll: Position = { - // x: Math.max(0, maxScroll.x), - // y: Math.max(0, maxScroll.y), - // }; + const adjustedMaxScroll: Position = { + x: Math.max(0, maxScroll.x), + y: Math.max(0, maxScroll.y), + }; - return maxScroll; + return adjustedMaxScroll; + + // return maxScroll; }; diff --git a/src/state/get-scroll-safe-area-for-element.js b/src/state/get-scroll-safe-area-for-element.js deleted file mode 100644 index f191dd8fb9..0000000000 --- a/src/state/get-scroll-safe-area-for-element.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import type { Area } from '../types'; -import getArea from './get-area'; - -export default (el: HTMLElement): Area => { - const top: number = el.scrollTop; - const left: number = el.scrollLeft; - const width: number = el.clientWidth; - const height: number = el.clientHeight; - - // computed - const right: number = left + width; - const bottom: number = top + height; - - return getArea({ - top, left, right, bottom, - }); -}; - diff --git a/src/state/position.js b/src/state/position.js index 0b28eaf86e..2d548a28a3 100644 --- a/src/state/position.js +++ b/src/state/position.js @@ -51,3 +51,10 @@ export const distance = (point1: Position, point2: Position): number => // When given a list of points, it finds the smallest distance to any point export const closest = (target: Position, points: Position[]): number => Math.min(...points.map((point: Position) => distance(target, point))); + +// used to apply any function to both values of a point +// eg: const floor = apply(Math.floor)(point); +export const apply = (fn: (value: number) => number) => (point: Position): Position => ({ + x: fn(point.x), + y: fn(point.y), +}); diff --git a/src/state/visibility/get-viewport.js b/src/state/visibility/get-viewport.js index c11e3c37aa..b986babfac 100644 --- a/src/state/visibility/get-viewport.js +++ b/src/state/visibility/get-viewport.js @@ -1,6 +1,21 @@ // @flow import type { Area } from '../../types'; -import getScrollSafeAreaForElement from '../get-scroll-safe-area-for-element'; +import getArea from '../get-area'; -export default (): Area => - getScrollSafeAreaForElement((document.documentElement: any)); +export default (): Area => { + const el: HTMLElement = (document.documentElement : any); + + // this will change as the element scrolls + const top: number = el.scrollTop; + const left: number = el.scrollLeft; + const width: number = el.clientWidth; + const height: number = el.clientHeight; + + // computed + const right: number = left + width; + const bottom: number = top + height; + + return getArea({ + top, left, right, bottom, + }); +}; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 678b5921a9..b6cde3e352 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -9,7 +9,6 @@ import getArea from '../../state/get-area'; import { getDroppableDimension } from '../../state/dimension'; import getClosestScrollable from '../get-closest-scrollable'; import { dimensionMarshalKey } from '../context-keys'; -import getScrollSafeAreaForElement from '../../state/get-scroll-safe-area-for-element'; import type { DimensionMarshal, DroppableCallbacks, @@ -38,6 +37,21 @@ type Props = {| const origin: Position = { x: 0, y: 0 }; +const getSafeScrollArea = (el: HTMLElement): Area => { + const top: number = el.offsetTop; + const left: number = el.offsetLeft; + const width: number = el.clientWidth; + const height: number = el.clientHeight; + + // computed + const right: number = left + width; + const bottom: number = top + height; + + return getArea({ + top, left, right, bottom, + }); +}; + export default class DroppableDimensionPublisher extends Component { /* eslint-disable react/sort-comp */ closestScrollable: ?Element = null; @@ -270,12 +284,13 @@ export default class DroppableDimensionPublisher extends Component { return null; } - // TODO: add margin to client? - const frameClient: Area = getScrollSafeAreaForElement((closestScrollable: any)); + const frameClient: Area = getSafeScrollArea((closestScrollable: any)); const scroll: Position = this.getClosestScroll(); const scrollWidth: number = closestScrollable.scrollWidth; const scrollHeight: number = closestScrollable.scrollHeight; + console.log('frameClient', frameClient); + return { frameClient, scrollWidth, From 85045e18dc6c4cbcb52f460e5bfaedbd3d23c6f0 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 1 Feb 2018 17:21:33 +1100 Subject: [PATCH 043/163] removing comment --- src/state/auto-scroll-marshal/can-scroll.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll-marshal/can-scroll.js index 76067ceb85..acc449d1d2 100644 --- a/src/state/auto-scroll-marshal/can-scroll.js +++ b/src/state/auto-scroll-marshal/can-scroll.js @@ -61,7 +61,6 @@ const canScroll = ({ }; const target: Position = add(current, smallestChange); - console.log('floored target', floor(target)); if (isEqual(target, origin)) { return false; From 1c06e5c8cdcd15bf062e0f3c344c30437cce979d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 2 Feb 2018 07:53:34 +1100 Subject: [PATCH 044/163] floor to round --- src/state/auto-scroll-marshal/can-scroll.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll-marshal/can-scroll.js index acc449d1d2..d0e17b1ec6 100644 --- a/src/state/auto-scroll-marshal/can-scroll.js +++ b/src/state/auto-scroll-marshal/can-scroll.js @@ -27,26 +27,27 @@ const getSmallestSignedValue = (value: number) => { return value > 0 ? 1 : -1; }; -const floor = apply(Math.floor); +// TODO: should this be round or floor? +const round = apply(Math.round); const isTooFarBackInBothDirections = (targetScroll: Position): boolean => { - const floored: Position = floor(targetScroll); - return floored.y <= 0 && floored.x <= 0; + const rounded: Position = round(targetScroll); + return rounded.y <= 0 && rounded.x <= 0; }; const isTooFarForwardInBothDirections = (targetScroll: Position, maxScroll: Position): boolean => { - const floored: Position = floor(targetScroll); - return floored.y >= maxScroll.y && floored.x >= maxScroll.x; + const rounded: Position = round(targetScroll); + return rounded.y >= maxScroll.y && rounded.x >= maxScroll.x; }; const isTooFarBackInEitherDirection = (targetScroll: Position): boolean => { - const floored: Position = floor(targetScroll); - return floored.y < 0 || floored.x < 0; + const rounded: Position = round(targetScroll); + return rounded.y < 0 || rounded.x < 0; }; const isTooFarForwardInEitherDirection = (targetScroll: Position, maxScroll: Position): boolean => { - const floored: Position = floor(targetScroll); - return floored.y > maxScroll.y || floored.x > maxScroll.x; + const rounded: Position = round(targetScroll); + return rounded.y > maxScroll.y || rounded.x > maxScroll.x; }; const canScroll = ({ From b82316427cdaace6e3a8f4577aff0561902441c3 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 2 Feb 2018 08:21:29 +1100 Subject: [PATCH 045/163] splitting out auto scroll marshal --- .../auto-scroll-marshal.js | 287 +----------------- .../create-fluid-scroller.js | 209 +++++++++++++ .../create-jump-scroller.js | 122 ++++++++ .../is-too-big-to-auto-scroll.js | 5 + 4 files changed, 350 insertions(+), 273 deletions(-) create mode 100644 src/state/auto-scroll-marshal/create-fluid-scroller.js create mode 100644 src/state/auto-scroll-marshal/create-jump-scroller.js create mode 100644 src/state/auto-scroll-marshal/is-too-big-to-auto-scroll.js diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll-marshal/auto-scroll-marshal.js index e4287cb230..abc1b17f0a 100644 --- a/src/state/auto-scroll-marshal/auto-scroll-marshal.js +++ b/src/state/auto-scroll-marshal/auto-scroll-marshal.js @@ -1,292 +1,34 @@ // @flow -import rafSchd from 'raf-schd'; -import getViewport from '../visibility/get-viewport'; -import { add, isEqual } from '../position'; -import { vertical, horizontal } from '../axis'; -import getScrollableDroppableOver from './get-scrollable-droppable-over'; -import { - canScrollDroppable, - canScrollWindow, - getWindowOverlap, - getDroppableOverlap, -} from './can-scroll'; import scrollWindow from './scroll-window'; -import getWindowScrollPosition from '../../view/get-window-scroll-position'; +import createFluidScroller, { type FluidScroller } from './create-fluid-scroller'; +import createJumpScroller, { type JumpScroller } from './create-jump-scroller'; +import { move as moveAction } from '../action-creators'; import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; import type { - Area, - Axis, DroppableId, - DragState, - DroppableDimension, Position, State, - Spacing, - DraggableLocation, - DraggableDimension, - ClosestScrollable, - DraggableId, } from '../../types'; type Args = {| scrollDroppable: (id: DroppableId, change: Position) => void, - move: (id: DraggableId, client: Position, windowScroll: Position, shouldAnimate: boolean) => void, + move: typeof moveAction, |} -// Values used to control how the fluid auto scroll feels -const config = { - // percentage distance from edge of container: - startFrom: 0.25, - maxSpeedAt: 0.05, - // pixels per frame - maxScrollSpeed: 28, - // A function used to ease the distance been the startFrom and maxSpeedAt values - // A simple linear function would be: (percentage) => percentage; - // percentage is between 0 and 1 - // result must be between 0 and 1 - ease: (percentage: number) => Math.pow(percentage, 2), -}; - -const origin: Position = { x: 0, y: 0 }; - -type PixelThresholds = {| - startFrom: number, - maxSpeedAt: number, - accelerationPlane: number, -|} - -// converts the percentages in the config into actual pixel values -const getPixelThresholds = (container: Area, axis: Axis): PixelThresholds => { - const startFrom: number = container[axis.size] * config.startFrom; - const maxSpeedAt: number = container[axis.size] * config.maxSpeedAt; - const accelerationPlane: number = startFrom - maxSpeedAt; - - const thresholds: PixelThresholds = { - startFrom, - maxSpeedAt, - accelerationPlane, - }; - - return thresholds; -}; - -const getSpeed = (distance: number, thresholds: PixelThresholds): number => { - // Not close enough to the edge - if (distance >= thresholds.startFrom) { - return 0; - } - - // Already past the maxSpeedAt point - - if (distance <= thresholds.maxSpeedAt) { - return config.maxScrollSpeed; - } - - // We need to perform a scroll as a percentage of the max scroll speed - - const distancePastStart: number = thresholds.startFrom - distance; - const percentage: number = distancePastStart / thresholds.accelerationPlane; - const transformed: number = config.ease(percentage); - - const speed: number = config.maxScrollSpeed * transformed; - - return speed; -}; - -// returns null if no scroll is required -const getRequiredScroll = (container: Area, center: Position): ?Position => { - // get distance to each edge - const distance: Spacing = { - top: center.y - container.top, - right: container.right - center.x, - bottom: container.bottom - center.y, - left: center.x - container.left, - }; - - // 1. Figure out which x,y values are the best target - // 2. Can the container scroll in that direction at all? - // If no for both directions, then return null - // 3. Is the center close enough to a edge to start a drag? - // 4. Based on the distance, calculate the speed at which a scroll should occur - // The lower distance value the faster the scroll should be. - // Maximum speed value should be hit before the distance is 0 - // Negative values to not continue to increase the speed - - const y: number = (() => { - const thresholds: PixelThresholds = getPixelThresholds(container, vertical); - const isCloserToBottom: boolean = distance.bottom < distance.top; - - if (isCloserToBottom) { - return getSpeed(distance.bottom, thresholds); - } - - // closer to top - return -1 * getSpeed(distance.top, thresholds); - })(); - - const x: number = (() => { - const thresholds: PixelThresholds = getPixelThresholds(container, horizontal); - const isCloserToRight: boolean = distance.right < distance.left; - - if (isCloserToRight) { - return getSpeed(distance.right, thresholds); - } - - // closer to left - return -1 * getSpeed(distance.left, thresholds); - })(); - - const required: Position = { x, y }; - - return isEqual(required, origin) ? null : required; -}; - -const isTooBigForAutoScrolling = (frame: Area, subject: Area): boolean => - subject.width > frame.width || subject.height > frame.height; - export default ({ scrollDroppable, move, }: Args): AutoScrollMarshal => { - // TODO: do not scroll if drag has finished - const scheduleWindowScroll = rafSchd(scrollWindow); - const scheduleDroppableScroll = rafSchd(scrollDroppable); - - const fluidScroll = (state: State) => { - const drag: ?DragState = state.drag; - if (!drag) { - console.error('Invalid drag state'); - return; - } - - const center: Position = drag.current.page.center; - - // 1. Can we scroll the viewport? - - const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; - const viewport: Area = getViewport(); - - if (isTooBigForAutoScrolling(viewport, draggable.page.withMargin)) { - return; - } - - const requiredWindowScroll: ?Position = getRequiredScroll(viewport, center); - - if (requiredWindowScroll && canScrollWindow(requiredWindowScroll)) { - console.log('scheduling window scroll', requiredWindowScroll); - scheduleWindowScroll(requiredWindowScroll); - return; - } - - // 2. We are not scrolling the window. Can we scroll the Droppable? + const fluidScroll: FluidScroller = createFluidScroller({ + scrollWindow, + scrollDroppable, + }); - const droppable: ?DroppableDimension = getScrollableDroppableOver({ - target: center, - droppables: state.dimension.droppable, - }); - - // No scrollable targets - if (!droppable) { - return; - } - - // We know this has a closestScrollable - const closestScrollable: ClosestScrollable = (droppable.viewport.closestScrollable : any); - - if (isTooBigForAutoScrolling(closestScrollable.frame, draggable.page.withMargin)) { - return; - } - - const requiredFrameScroll: ?Position = getRequiredScroll(closestScrollable.frame, center); - - if (requiredFrameScroll && canScrollDroppable(droppable, requiredFrameScroll)) { - scheduleDroppableScroll(droppable.descriptor.id, requiredFrameScroll); - } - }; - - const performMove = (state: State, offset: Position) => { - const drag: ?DragState = state.drag; - if (!drag) { - return; - } - - const client: Position = add(drag.current.client.selection, offset); - move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); - }; - - const jumpScroll = (state: State) => { - const drag: ?DragState = state.drag; - - if (!drag) { - return; - } - - const request: ?Position = drag.scrollJumpRequest; - - if (!request) { - return; - } - - const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; - const destination: ?DraggableLocation = drag.impact.destination; - - if (!destination) { - console.error('Cannot perform a jump scroll when there is no destination'); - return; - } - - const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; - const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; - - if (closestScrollable) { - if (isTooBigForAutoScrolling(closestScrollable.frame, draggable.page.withMargin)) { - performMove(state, request); - return; - } - - if (canScrollDroppable(droppable, request)) { - // not scheduling - jump requests need to be performed instantly - - // if the window can also not be scrolled - adjust the item - if (!canScrollWindow(request)) { - const overlap: ?Position = getDroppableOverlap(droppable, request); - - if (overlap) { - console.warn('DROPPABLE OVERLAP', overlap); - performMove(state, overlap); - } - } - - scrollDroppable(droppable.descriptor.id, request); - return; - } - - // can now check if we need to scroll the window - } - - // Scroll the window if we can - - if (isTooBigForAutoScrolling(getViewport(), draggable.page.withMargin)) { - performMove(state, request); - return; - } - - if (!canScrollWindow(request)) { - console.warn('Jump scroll requested but it cannot be done by Droppable or the Window'); - performMove(state, request); - return; - } - - const overlap: ?Position = getWindowOverlap(request); - - if (overlap) { - console.warn('WINDOW OVERLAP', overlap); - performMove(state, overlap); - } - - // not scheduling - jump requests need to be performed instantly - scrollWindow(request); - }; + const jumpScroll: JumpScroller = createJumpScroller({ + move, + scrollWindow, + scrollDroppable, + }); const onStateChange = (previous: State, current: State): void => { // now dragging @@ -312,8 +54,7 @@ export default ({ // cancel any pending scrolls if no longer dragging if (previous.phase === 'DRAGGING' && current.phase !== 'DRAGGING') { - scheduleWindowScroll.cancel(); - scheduleDroppableScroll.cancel(); + fluidScroll.cancel(); } }; diff --git a/src/state/auto-scroll-marshal/create-fluid-scroller.js b/src/state/auto-scroll-marshal/create-fluid-scroller.js new file mode 100644 index 0000000000..3365cb6cdd --- /dev/null +++ b/src/state/auto-scroll-marshal/create-fluid-scroller.js @@ -0,0 +1,209 @@ +// @flow +import rafSchd from 'raf-schd'; +import getViewport from '../visibility/get-viewport'; +import { isEqual } from '../position'; +import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; +import getScrollableDroppableOver from './get-scrollable-droppable-over'; +import { horizontal, vertical } from '../axis'; +import { + canScrollDroppable, + canScrollWindow, +} from './can-scroll'; +import type { + Area, + Axis, + Spacing, + DroppableId, + DragState, + DroppableDimension, + Position, + State, + DraggableDimension, + ClosestScrollable, +} from '../../types'; + +// Values used to control how the fluid auto scroll feels +const config = { + // percentage distance from edge of container: + startFrom: 0.25, + maxSpeedAt: 0.05, + // pixels per frame + maxScrollSpeed: 28, + // A function used to ease the distance been the startFrom and maxSpeedAt values + // A simple linear function would be: (percentage) => percentage; + // percentage is between 0 and 1 + // result must be between 0 and 1 + ease: (percentage: number) => Math.pow(percentage, 2), +}; + +const origin: Position = { x: 0, y: 0 }; + +type PixelThresholds = {| + startFrom: number, + maxSpeedAt: number, + accelerationPlane: number, +|} + +// converts the percentages in the config into actual pixel values +const getPixelThresholds = (container: Area, axis: Axis): PixelThresholds => { + const startFrom: number = container[axis.size] * config.startFrom; + const maxSpeedAt: number = container[axis.size] * config.maxSpeedAt; + const accelerationPlane: number = startFrom - maxSpeedAt; + + const thresholds: PixelThresholds = { + startFrom, + maxSpeedAt, + accelerationPlane, + }; + + return thresholds; +}; + +const getSpeed = (distance: number, thresholds: PixelThresholds): number => { + // Not close enough to the edge + if (distance >= thresholds.startFrom) { + return 0; + } + + // Already past the maxSpeedAt point + + if (distance <= thresholds.maxSpeedAt) { + return config.maxScrollSpeed; + } + + // We need to perform a scroll as a percentage of the max scroll speed + + const distancePastStart: number = thresholds.startFrom - distance; + const percentage: number = distancePastStart / thresholds.accelerationPlane; + const transformed: number = config.ease(percentage); + + const speed: number = config.maxScrollSpeed * transformed; + + return speed; +}; + +// returns null if no scroll is required +const getRequiredScroll = (container: Area, center: Position): ?Position => { + // get distance to each edge + const distance: Spacing = { + top: center.y - container.top, + right: container.right - center.x, + bottom: container.bottom - center.y, + left: center.x - container.left, + }; + + // 1. Figure out which x,y values are the best target + // 2. Can the container scroll in that direction at all? + // If no for both directions, then return null + // 3. Is the center close enough to a edge to start a drag? + // 4. Based on the distance, calculate the speed at which a scroll should occur + // The lower distance value the faster the scroll should be. + // Maximum speed value should be hit before the distance is 0 + // Negative values to not continue to increase the speed + + const y: number = (() => { + const thresholds: PixelThresholds = getPixelThresholds(container, vertical); + const isCloserToBottom: boolean = distance.bottom < distance.top; + + if (isCloserToBottom) { + return getSpeed(distance.bottom, thresholds); + } + + // closer to top + return -1 * getSpeed(distance.top, thresholds); + })(); + + const x: number = (() => { + const thresholds: PixelThresholds = getPixelThresholds(container, horizontal); + const isCloserToRight: boolean = distance.right < distance.left; + + if (isCloserToRight) { + return getSpeed(distance.right, thresholds); + } + + // closer to left + return -1 * getSpeed(distance.left, thresholds); + })(); + + const required: Position = { x, y }; + + return isEqual(required, origin) ? null : required; +}; + +type Api = {| + scrollWindow: (offset: Position) => void, + scrollDroppable: (id: DroppableId, offset: Position) => void, +|} + +type ResultFn = (state: State) => void; +type ResultCancel = { cancel: () => void }; + +export type FluidScroller = ResultFn & ResultCancel; + +export default ({ + scrollWindow, + scrollDroppable, +}: Api): FluidScroller => { + const scheduleWindowScroll = rafSchd(scrollWindow); + const scheduleDroppableScroll = rafSchd(scrollDroppable); + + const result = (state: State): void => { + const drag: ?DragState = state.drag; + if (!drag) { + console.error('Invalid drag state'); + return; + } + + const center: Position = drag.current.page.center; + + // 1. Can we scroll the viewport? + + const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; + const viewport: Area = getViewport(); + + if (isTooBigToAutoScroll(viewport, draggable.page.withMargin)) { + return; + } + + const requiredWindowScroll: ?Position = getRequiredScroll(viewport, center); + + if (requiredWindowScroll && canScrollWindow(requiredWindowScroll)) { + console.log('scheduling window scroll', requiredWindowScroll); + scheduleWindowScroll(requiredWindowScroll); + return; + } + + // 2. We are not scrolling the window. Can we scroll the Droppable? + + const droppable: ?DroppableDimension = getScrollableDroppableOver({ + target: center, + droppables: state.dimension.droppable, + }); + + // No scrollable targets + if (!droppable) { + return; + } + + // We know this has a closestScrollable + const closestScrollable: ClosestScrollable = (droppable.viewport.closestScrollable : any); + + if (isTooBigToAutoScroll(closestScrollable.frame, draggable.page.withMargin)) { + return; + } + + const requiredFrameScroll: ?Position = getRequiredScroll(closestScrollable.frame, center); + + if (requiredFrameScroll && canScrollDroppable(droppable, requiredFrameScroll)) { + scheduleDroppableScroll(droppable.descriptor.id, requiredFrameScroll); + } + }; + + result.cancel = () => { + scheduleWindowScroll.cancel(); + scheduleDroppableScroll.cancel(); + }; + + return result; +}; + diff --git a/src/state/auto-scroll-marshal/create-jump-scroller.js b/src/state/auto-scroll-marshal/create-jump-scroller.js new file mode 100644 index 0000000000..a69e7daee2 --- /dev/null +++ b/src/state/auto-scroll-marshal/create-jump-scroller.js @@ -0,0 +1,122 @@ +// @flow +import { add } from '../position'; +import getWindowScrollPosition from '../../view/get-window-scroll-position'; +import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; +import getViewport from '../visibility/get-viewport'; +import { move as moveAction } from '../action-creators'; +import { + canScrollDroppable, + canScrollWindow, + getWindowOverlap, + getDroppableOverlap, +} from './can-scroll'; +import type { + DroppableId, + DragState, + DroppableDimension, + Position, + State, + DraggableLocation, + DraggableDimension, + ClosestScrollable, +} from '../../types'; + +type Args = {| + scrollDroppable: (id: DroppableId, offset: Position) => void, + scrollWindow: (offset: Position) => void, + move: typeof moveAction, +|} + +export type JumpScroller = (state: State) => void; + +export default ({ + move, + scrollDroppable, + scrollWindow, +}: Args): JumpScroller => { + const moveByOffset = (state: State, offset: Position) => { + const drag: ?DragState = state.drag; + if (!drag) { + return; + } + + const client: Position = add(drag.current.client.selection, offset); + move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); + }; + + const jumpScroller: JumpScroller = (state: State) => { + const drag: ?DragState = state.drag; + + if (!drag) { + return; + } + + const request: ?Position = drag.scrollJumpRequest; + + if (!request) { + return; + } + + const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; + const destination: ?DraggableLocation = drag.impact.destination; + + if (!destination) { + console.error('Cannot perform a jump scroll when there is no destination'); + return; + } + + const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; + const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; + + if (closestScrollable) { + if (isTooBigToAutoScroll(closestScrollable.frame, draggable.page.withMargin)) { + moveByOffset(state, request); + return; + } + + if (canScrollDroppable(droppable, request)) { + // not scheduling - jump requests need to be performed instantly + + // if the window can also not be scrolled - adjust the item + if (!canScrollWindow(request)) { + const overlap: ?Position = getDroppableOverlap(droppable, request); + + if (overlap) { + console.warn('DROPPABLE OVERLAP', overlap); + moveByOffset(state, overlap); + } + } + + scrollDroppable(droppable.descriptor.id, request); + return; + } + + // can now check if we need to scroll the window + } + + // Scroll the window if we can + + if (isTooBigToAutoScroll(getViewport(), draggable.page.withMargin)) { + moveByOffset(state, request); + return; + } + + if (!canScrollWindow(request)) { + console.warn('Jump scroll requested but it cannot be done by Droppable or the Window'); + moveByOffset(state, request); + return; + } + + const overlap: ?Position = getWindowOverlap(request); + + if (overlap) { + console.warn('WINDOW OVERLAP', overlap); + moveByOffset(state, overlap); + } + + // not scheduling - jump requests need to be performed instantly + scrollWindow(request); + }; + + return jumpScroller; +}; diff --git a/src/state/auto-scroll-marshal/is-too-big-to-auto-scroll.js b/src/state/auto-scroll-marshal/is-too-big-to-auto-scroll.js new file mode 100644 index 0000000000..24c5f6502f --- /dev/null +++ b/src/state/auto-scroll-marshal/is-too-big-to-auto-scroll.js @@ -0,0 +1,5 @@ +// @flow +import type { Area } from '../../types'; + +export default (frame: Area, subject: Area): boolean => + subject.width > frame.width || subject.height > frame.height; From b15cd43e912279fc26eb8b41119fa9875681bfd0 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 2 Feb 2018 14:01:38 +1100 Subject: [PATCH 046/163] more fiddling --- src/state/auto-scroll-marshal/can-scroll.js | 93 ++++++++++--------- .../create-jump-scroller.js | 6 ++ src/state/visibility/get-viewport.js | 21 +++-- .../droppable-dimension-publisher.jsx | 7 +- src/view/get-window-scroll-position.js | 39 +++++--- 5 files changed, 103 insertions(+), 63 deletions(-) diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll-marshal/can-scroll.js index d0e17b1ec6..a83d447c93 100644 --- a/src/state/auto-scroll-marshal/can-scroll.js +++ b/src/state/auto-scroll-marshal/can-scroll.js @@ -20,35 +20,48 @@ type CanScrollArgs = {| const origin: Position = { x: 0, y: 0 }; -const getSmallestSignedValue = (value: number) => { +// TODO: should this be round or floor? +const round = apply(Math.round); +const floor = apply(Math.floor); +const smallestSigned = apply((value: number) => { if (value === 0) { return 0; } return value > 0 ? 1 : -1; -}; +}); -// TODO: should this be round or floor? -const round = apply(Math.round); +const isTooFarBack = (targetScroll: Position): boolean => { + const floored: Position = floor(targetScroll); + console.log('floored', floored); -const isTooFarBackInBothDirections = (targetScroll: Position): boolean => { - const rounded: Position = round(targetScroll); - return rounded.y <= 0 && rounded.x <= 0; + return floored.x < 0 || floored.y < 0; }; -const isTooFarForwardInBothDirections = (targetScroll: Position, maxScroll: Position): boolean => { - const rounded: Position = round(targetScroll); - return rounded.y >= maxScroll.y && rounded.x >= maxScroll.x; -}; +const isTooFarForward = (targetScroll: Position, maxScroll: Position): boolean => { + const floored: Position = floor(targetScroll); -const isTooFarBackInEitherDirection = (targetScroll: Position): boolean => { - const rounded: Position = round(targetScroll); - return rounded.y < 0 || rounded.x < 0; + return floored.x > maxScroll.x || floored.y > maxScroll.y; }; -const isTooFarForwardInEitherDirection = (targetScroll: Position, maxScroll: Position): boolean => { - const rounded: Position = round(targetScroll); - return rounded.y > maxScroll.y || rounded.x > maxScroll.x; -}; +// const isTooFarBackInBothDirections = (targetScroll: Position): boolean => { +// const rounded: Position = round(targetScroll); +// return rounded.y < 0 && rounded.x < 0; +// }; + +// const isTooFarForwardInBothDirections = (targetScroll: Position, maxScroll: Position): boolean => { +// const rounded: Position = round(targetScroll); +// return rounded.y > maxScroll.y && rounded.x > maxScroll.x; +// }; + +// const isTooFarBackInEitherDirection = (targetScroll: Position): boolean => { +// const rounded: Position = round(targetScroll); +// return rounded.y < 0 || rounded.x < 0; +// }; + +// const isTooFarForwardInEitherDirection = (targetScroll: Position, maxScroll: Position): boolean => { +// const rounded: Position = round(targetScroll); +// return rounded.y > maxScroll.y || rounded.x > maxScroll.x; +// }; const canScroll = ({ max, @@ -56,26 +69,27 @@ const canScroll = ({ change, }: CanScrollArgs): boolean => { // Only need to be able to move the smallest amount in the desired direction - const smallestChange: Position = { - x: getSmallestSignedValue(change.x), - y: getSmallestSignedValue(change.y), - }; + const smallestChange: Position = smallestSigned(change); + const targetScroll: Position = add(current, smallestChange); - const target: Position = add(current, smallestChange); - - if (isEqual(target, origin)) { + if (isEqual(targetScroll, origin)) { return false; } + console.group('canScroll?'); console.log('smallest change', smallestChange); + console.log('current', current); + console.log('target', targetScroll); + console.log('max', max); + console.groupEnd(); - if (isTooFarBackInBothDirections(target)) { - console.log('too far back', { target }); + if (isTooFarBack(targetScroll)) { + console.log('too far back', { targetScroll }); return false; } - if (isTooFarForwardInBothDirections(target, max)) { - console.log('too far forward', { target, max }); + if (isTooFarForward(targetScroll, max)) { + console.log('too far forward', { targetScroll, max }); return false; } @@ -109,15 +123,7 @@ export const canScrollWindow = (change: Position): boolean => { const maxScroll: Position = getMaxWindowScroll(); const currentScroll: Position = getWindowScrollPosition(); - console.group('can scroll window'); - console.log('max scroll', maxScroll); - console.log('current', currentScroll); - console.log('can scroll?', canScroll({ - current: currentScroll, - max: maxScroll, - change, - })); - console.groupEnd(); + console.warn('can scroll window?'); return canScroll({ current: currentScroll, @@ -156,15 +162,16 @@ const getOverlap = ({ max, change, }: GetOverlapArgs): ?Position => { - const target: Position = add(current, change); - console.log('getting overlap'); + const target: Position = apply((value: number) => + (value > 0 ? Math.floor(value) : Math.ceil(value)) + )(change); - if (isTooFarBackInEitherDirection(target)) { + if (isTooFarBack(target)) { console.log('backward overlap'); return target; } - if (isTooFarForwardInEitherDirection(target, max)) { + if (isTooFarForward(target, max)) { const trimmedMax: Position = { x: target.x === 0 ? 0 : max.x, y: target.y === 0 ? 0 : max.y, @@ -186,6 +193,7 @@ export const getWindowOverlap = (change: Position): ?Position => { const max: Position = getMaxWindowScroll(); const current: Position = getWindowScrollPosition(); + console.warn('getting window overlap'); return getOverlap({ current, max, @@ -204,6 +212,7 @@ export const getDroppableOverlap = (droppable: DroppableDimension, change: Posit return null; } + console.log('getting droppable overlap'); return getOverlap({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, diff --git a/src/state/auto-scroll-marshal/create-jump-scroller.js b/src/state/auto-scroll-marshal/create-jump-scroller.js index a69e7daee2..b190bbe8b6 100644 --- a/src/state/auto-scroll-marshal/create-jump-scroller.js +++ b/src/state/auto-scroll-marshal/create-jump-scroller.js @@ -40,6 +40,8 @@ export default ({ return; } + console.warn('moving by offset', offset); + const client: Position = add(drag.current.client.selection, offset); move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); }; @@ -81,10 +83,14 @@ export default ({ if (!canScrollWindow(request)) { const overlap: ?Position = getDroppableOverlap(droppable, request); + console.log('droppable overlap?', overlap); + if (overlap) { console.warn('DROPPABLE OVERLAP', overlap); moveByOffset(state, overlap); } + } else { + console.log('can still scroll window'); } scrollDroppable(droppable.descriptor.id, request); diff --git a/src/state/visibility/get-viewport.js b/src/state/visibility/get-viewport.js index b986babfac..df9785a65a 100644 --- a/src/state/visibility/get-viewport.js +++ b/src/state/visibility/get-viewport.js @@ -1,15 +1,22 @@ // @flow -import type { Area } from '../../types'; +import type { Position, Area } from '../../types'; import getArea from '../get-area'; +import getWindowScrollPosition from '../../view/get-window-scroll-position'; export default (): Area => { - const el: HTMLElement = (document.documentElement : any); + const windowScroll: Position = getWindowScrollPosition(); - // this will change as the element scrolls - const top: number = el.scrollTop; - const left: number = el.scrollLeft; - const width: number = el.clientWidth; - const height: number = el.clientHeight; + const top: number = windowScroll.y; + const left: number = windowScroll.x; + + const doc: HTMLElement = (document.documentElement : any); + + console.log('doc top', document.documentElement.scrollTop); + console.log('window', window.pageYOffset); + + // using these values as they do not consider scrollbars + const width: number = doc.clientWidth; + const height: number = doc.clientHeight; // computed const right: number = left + width; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index b6cde3e352..b13921fb9e 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -9,6 +9,7 @@ import getArea from '../../state/get-area'; import { getDroppableDimension } from '../../state/dimension'; import getClosestScrollable from '../get-closest-scrollable'; import { dimensionMarshalKey } from '../context-keys'; +import { apply } from '../../state/position'; import type { DimensionMarshal, DroppableCallbacks, @@ -36,6 +37,7 @@ type Props = {| |} const origin: Position = { x: 0, y: 0 }; +const floor = apply(Math.floor); const getSafeScrollArea = (el: HTMLElement): Area => { const top: number = el.offsetTop; @@ -79,10 +81,11 @@ export default class DroppableDimensionPublisher extends Component { return origin; } - const offset: Position = { + // We are using the floor of all scroll values + const offset: Position = floor({ x: this.closestScrollable.scrollLeft, y: this.closestScrollable.scrollTop, - }; + }); return offset; } diff --git a/src/view/get-window-scroll-position.js b/src/view/get-window-scroll-position.js index 5744d3ba88..fa4dcf6576 100644 --- a/src/view/get-window-scroll-position.js +++ b/src/view/get-window-scroll-position.js @@ -1,19 +1,34 @@ // @flow +import { apply } from '../state/position'; import type { Position } from '../types'; -const origin: Position = { x: 0, y: 0 }; +const floor = apply(Math.floor); -export default (): Position => { - const el: ?HTMLElement = document.documentElement; +// The browsers update document.documentElement.scrollTop and window.pageYOffset +// differently as the window scrolls. - // should never happen - if (!el) { - return origin; - } +// Webkit +// documentElement.scrollTop: no update. Stays at 0 +// window.pageYOffset: updates to whole number - return { - x: el.scrollLeft, - y: el.scrollTop, - }; -}; +// Chrome +// documentElement.scrollTop: update with fractional value +// window.pageYOffset: update with fractional value + +// FireFox +// documentElement.scrollTop: updates to whole number +// window.pageYOffset: updates to whole number + +// IE11 (same as firefox) +// documentElement.scrollTop: updates to whole number +// window.pageYOffset: updates to whole number + +// Edge (same as webkit) +// documentElement.scrollTop: no update. Stays at 0 +// window.pageYOffset: updates to whole number + +export default (): Position => floor({ + x: window.pageXOffset, + y: window.pageYOffset, +}); From 0cce0976b9aaf6206326e7b18dfd14e01b336b5f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 2 Feb 2018 16:53:33 +1100 Subject: [PATCH 047/163] initial tests --- .../auto-scroll-marshal-types.js | 0 .../auto-scroll-marshal.js | 0 .../can-partially-scroll.js} | 104 +++----- .../create-fluid-scroller.js | 0 .../create-jump-scroller.js | 0 .../get-scrollable-droppable-over.js | 0 .../is-too-big-to-auto-scroll.js | 0 .../scroll-window.js | 0 .../droppable-dimension-publisher.jsx | 5 +- .../auto-scroll/can-partially-scroll.spec.js | 250 ++++++++++++++++++ 10 files changed, 291 insertions(+), 68 deletions(-) rename src/state/{auto-scroll-marshal => auto-scroll}/auto-scroll-marshal-types.js (100%) rename src/state/{auto-scroll-marshal => auto-scroll}/auto-scroll-marshal.js (100%) rename src/state/{auto-scroll-marshal/can-scroll.js => auto-scroll/can-partially-scroll.js} (61%) rename src/state/{auto-scroll-marshal => auto-scroll}/create-fluid-scroller.js (100%) rename src/state/{auto-scroll-marshal => auto-scroll}/create-jump-scroller.js (100%) rename src/state/{auto-scroll-marshal => auto-scroll}/get-scrollable-droppable-over.js (100%) rename src/state/{auto-scroll-marshal => auto-scroll}/is-too-big-to-auto-scroll.js (100%) rename src/state/{auto-scroll-marshal => auto-scroll}/scroll-window.js (100%) create mode 100644 test/unit/state/auto-scroll/can-partially-scroll.spec.js diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal-types.js b/src/state/auto-scroll/auto-scroll-marshal-types.js similarity index 100% rename from src/state/auto-scroll-marshal/auto-scroll-marshal-types.js rename to src/state/auto-scroll/auto-scroll-marshal-types.js diff --git a/src/state/auto-scroll-marshal/auto-scroll-marshal.js b/src/state/auto-scroll/auto-scroll-marshal.js similarity index 100% rename from src/state/auto-scroll-marshal/auto-scroll-marshal.js rename to src/state/auto-scroll/auto-scroll-marshal.js diff --git a/src/state/auto-scroll-marshal/can-scroll.js b/src/state/auto-scroll/can-partially-scroll.js similarity index 61% rename from src/state/auto-scroll-marshal/can-scroll.js rename to src/state/auto-scroll/can-partially-scroll.js index a83d447c93..2917c39c33 100644 --- a/src/state/auto-scroll-marshal/can-scroll.js +++ b/src/state/auto-scroll/can-partially-scroll.js @@ -21,8 +21,6 @@ type CanScrollArgs = {| const origin: Position = { x: 0, y: 0 }; // TODO: should this be round or floor? -const round = apply(Math.round); -const floor = apply(Math.floor); const smallestSigned = apply((value: number) => { if (value === 0) { return 0; @@ -30,66 +28,31 @@ const smallestSigned = apply((value: number) => { return value > 0 ? 1 : -1; }); -const isTooFarBack = (targetScroll: Position): boolean => { - const floored: Position = floor(targetScroll); - console.log('floored', floored); +const isTooFarBack = (targetScroll: Position): boolean => + targetScroll.x < 0 || targetScroll.y < 0; - return floored.x < 0 || floored.y < 0; -}; - -const isTooFarForward = (targetScroll: Position, maxScroll: Position): boolean => { - const floored: Position = floor(targetScroll); - - return floored.x > maxScroll.x || floored.y > maxScroll.y; -}; - -// const isTooFarBackInBothDirections = (targetScroll: Position): boolean => { -// const rounded: Position = round(targetScroll); -// return rounded.y < 0 && rounded.x < 0; -// }; - -// const isTooFarForwardInBothDirections = (targetScroll: Position, maxScroll: Position): boolean => { -// const rounded: Position = round(targetScroll); -// return rounded.y > maxScroll.y && rounded.x > maxScroll.x; -// }; +const isTooFarForward = (targetScroll: Position, maxScroll: Position): boolean => + targetScroll.x > maxScroll.x || targetScroll.y > maxScroll.y; -// const isTooFarBackInEitherDirection = (targetScroll: Position): boolean => { -// const rounded: Position = round(targetScroll); -// return rounded.y < 0 || rounded.x < 0; -// }; - -// const isTooFarForwardInEitherDirection = (targetScroll: Position, maxScroll: Position): boolean => { -// const rounded: Position = round(targetScroll); -// return rounded.y > maxScroll.y || rounded.x > maxScroll.x; -// }; - -const canScroll = ({ +export const canPartiallyScroll = ({ max, current, change, }: CanScrollArgs): boolean => { + // Sure - you can move nowhere if you want + if (isEqual(change, origin)) { + return true; + } + // Only need to be able to move the smallest amount in the desired direction const smallestChange: Position = smallestSigned(change); const targetScroll: Position = add(current, smallestChange); - if (isEqual(targetScroll, origin)) { - return false; - } - - console.group('canScroll?'); - console.log('smallest change', smallestChange); - console.log('current', current); - console.log('target', targetScroll); - console.log('max', max); - console.groupEnd(); - if (isTooFarBack(targetScroll)) { - console.log('too far back', { targetScroll }); return false; } if (isTooFarForward(targetScroll, max)) { - console.log('too far forward', { targetScroll, max }); return false; } @@ -125,7 +88,7 @@ export const canScrollWindow = (change: Position): boolean => { console.warn('can scroll window?'); - return canScroll({ + return canPartiallyScroll({ current: currentScroll, max: maxScroll, change, @@ -144,7 +107,7 @@ export const canScrollDroppable = ( console.warn('can scroll droppable?'); - return canScroll({ + return canPartiallyScroll({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, change, @@ -157,28 +120,39 @@ type GetOverlapArgs = {| change: Position, |} -const getOverlap = ({ +// We need to figure out how much of the movement +// cannot be done with a scroll +export const getRemainder = ({ current, max, change, }: GetOverlapArgs): ?Position => { - const target: Position = apply((value: number) => - (value > 0 ? Math.floor(value) : Math.ceil(value)) - )(change); + const canScroll: boolean = canPartiallyScroll({ + current, max, change, + }); - if (isTooFarBack(target)) { - console.log('backward overlap'); - return target; + if (!canScroll) { + return null; } - if (isTooFarForward(target, max)) { - const trimmedMax: Position = { - x: target.x === 0 ? 0 : max.x, - y: target.y === 0 ? 0 : max.y, + const targetScroll: Position = add(current, change); + + if (isTooFarBack(targetScroll)) { + // if we are moving backwards, any value that is + // positive change be trimmed + const trimmed: Position = { + x: targetScroll.x > 0 ? 0 : targetScroll.x, + y: targetScroll.y > 0 ? 0 : targetScroll.y, + }; + return trimmed; + } + + if (isTooFarForward(targetScroll, max)) { + const trimmed: Position = { + x: targetScroll.x < max.x ? 0 : targetScroll.x - max.x, + y: targetScroll.y < max.y ? 0 : targetScroll.y - max.y, }; - const overlap: Position = subtract(target, trimmedMax); - console.log('forward overlap', target, overlap); - return overlap; + return trimmed; } // no overlap @@ -194,7 +168,7 @@ export const getWindowOverlap = (change: Position): ?Position => { const current: Position = getWindowScrollPosition(); console.warn('getting window overlap'); - return getOverlap({ + return getRemainder({ current, max, change, @@ -213,7 +187,7 @@ export const getDroppableOverlap = (droppable: DroppableDimension, change: Posit } console.log('getting droppable overlap'); - return getOverlap({ + return getRemainder({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, change, diff --git a/src/state/auto-scroll-marshal/create-fluid-scroller.js b/src/state/auto-scroll/create-fluid-scroller.js similarity index 100% rename from src/state/auto-scroll-marshal/create-fluid-scroller.js rename to src/state/auto-scroll/create-fluid-scroller.js diff --git a/src/state/auto-scroll-marshal/create-jump-scroller.js b/src/state/auto-scroll/create-jump-scroller.js similarity index 100% rename from src/state/auto-scroll-marshal/create-jump-scroller.js rename to src/state/auto-scroll/create-jump-scroller.js diff --git a/src/state/auto-scroll-marshal/get-scrollable-droppable-over.js b/src/state/auto-scroll/get-scrollable-droppable-over.js similarity index 100% rename from src/state/auto-scroll-marshal/get-scrollable-droppable-over.js rename to src/state/auto-scroll/get-scrollable-droppable-over.js diff --git a/src/state/auto-scroll-marshal/is-too-big-to-auto-scroll.js b/src/state/auto-scroll/is-too-big-to-auto-scroll.js similarity index 100% rename from src/state/auto-scroll-marshal/is-too-big-to-auto-scroll.js rename to src/state/auto-scroll/is-too-big-to-auto-scroll.js diff --git a/src/state/auto-scroll-marshal/scroll-window.js b/src/state/auto-scroll/scroll-window.js similarity index 100% rename from src/state/auto-scroll-marshal/scroll-window.js rename to src/state/auto-scroll/scroll-window.js diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index b13921fb9e..8ae48b37af 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -81,11 +81,10 @@ export default class DroppableDimensionPublisher extends Component { return origin; } - // We are using the floor of all scroll values - const offset: Position = floor({ + const offset: Position = { x: this.closestScrollable.scrollLeft, y: this.closestScrollable.scrollTop, - }); + }; return offset; } diff --git a/test/unit/state/auto-scroll/can-partially-scroll.spec.js b/test/unit/state/auto-scroll/can-partially-scroll.spec.js new file mode 100644 index 0000000000..6e0fdde4fd --- /dev/null +++ b/test/unit/state/auto-scroll/can-partially-scroll.spec.js @@ -0,0 +1,250 @@ +// @flow +import type { + Position, +} from '../../../../src/types'; +import { canPartiallyScroll, getRemainder } from '../../../../src/state/auto-scroll/can-partially-scroll'; +import { add, subtract } from '../../../../src/state/position'; + +const origin: Position = { x: 0, y: 0 }; + +describe('can partially scroll', () => { + it('should return true if not scrolling anywhere', () => { + const result: boolean = canPartiallyScroll({ + max: { x: 100, y: 100 }, + current: { x: 0, y: 0 }, + // not + change: origin, + }); + + expect(result).toBe(true); + }); + + it('should return true if scrolling to a boundary', () => { + const current: Position = origin; + const max: Position = { x: 100, y: 200 }; + + const corners: Position[] = [ + // top left + { x: 0, y: 0 }, + // top right + { x: max.x, y: 0 }, + // bottom right + { x: max.x, y: max.y }, + // bottom left + { x: 0, y: max.y }, + ]; + + corners.forEach((corner: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: corner, + }); + + expect(result).toBe(true); + }); + }); + + it('should return true if moving in any direction within the allowable scroll region', () => { + const max: Position = { x: 100, y: 100 }; + const current: Position = { x: 50, y: 50 }; + + // all of these movements are totally possible + const changes: Position[] = [ + // top left + { x: -10, y: 10 }, + // top right + { x: 10, y: 10 }, + // bottom right + { x: 10, y: -10 }, + // bottom left + { x: -10, y: -10 }, + ]; + + changes.forEach((point: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: point, + }); + + expect(result).toBe(true); + }); + }); + + it('should return true if able to partially move in both directions', () => { + const max: Position = { x: 100, y: 100 }; + const current: Position = { x: 50, y: 50 }; + + // all of these movements are partially possible + const changes: Position[] = [ + // top left + { x: -200, y: 200 }, + // top right + { x: 200, y: 200 }, + // bottom right + { x: 200, y: -200 }, + // bottom left + { x: -200, y: -200 }, + ]; + + changes.forEach((point: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: point, + }); + + expect(result).toBe(true); + }); + }); + + it('should return false if can only scroll in one direction', () => { + const max: Position = { x: 100, y: 200 }; + + type Item = {| + current: Position, + change: Position, + |} + + const changes: Item[] = [ + // Can move back in the y direction, but not back in the x direction + { + current: { x: 0, y: 1 }, + change: { x: -1, y: -1 }, + }, + // Can move back in the x direction, but not back in the y direction + { + current: { x: 1, y: 0 }, + change: { x: -1, y: -1 }, + }, + // Can move forward in the y direction, but not forward in the x direction + { + current: subtract(max, { x: 0, y: 1 }), + change: { x: 1, y: 1 }, + }, + // Can move forward in the x direction, but not forward in the y direction + { + current: subtract(max, { x: 1, y: 0 }), + change: { x: 1, y: 1 }, + }, + ]; + + changes.forEach((item: Item) => { + const result: boolean = canPartiallyScroll({ + max, + current: item.current, + change: item.change, + }); + + expect(result).toBe(false); + }); + }); + + it('should return false if on the min point and move backward in any direction', () => { + const current: Position = origin; + const max: Position = { x: 100, y: 200 }; + const tooFarBack: Position[] = [ + { x: 0, y: -1 }, + { x: -1, y: 0 }, + ]; + + tooFarBack.forEach((point: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: point, + }); + + expect(result).toBe(false); + }); + }); + + it('should return false if on the max point and move forward in any direction', () => { + const max: Position = { x: 100, y: 200 }; + const current: Position = max; + const tooFarForward: Position[] = [ + add(max, { x: 0, y: 1 }), + add(max, { x: 1, y: 0 }), + ]; + + tooFarForward.forEach((point: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: point, + }); + + expect(result).toBe(false); + }); + }); +}); + +describe('get overlap', () => { + it('should return null if you cannot partially scroll', () => { + // moving too far back + const current: Position = origin; + const max: Position = origin; + const tooFarBack: Position = { x: 0, y: -1 }; + + const result: ?Position = getRemainder({ + current, max, change: tooFarBack, + }); + + expect(result).toBe(null); + + // validating the result + + const validate: boolean = canPartiallyScroll({ + max, + current, + change: tooFarBack, + }); + + expect(validate).toBe(false); + }); + + it.only('should return the overlap', () => { + const max: Position = { x: 100, y: 100 }; + const current: Position = { x: 50, y: 50 }; + + type Item = {| + change: Position, + expected: Position, + |} + + const items: Item[] = [ + // too far back: top + { + change: { x: -20, y: -70 }, + expected: { x: 0, y: -20 }, + }, + // too far back: left + { + change: { x: -70, y: -40 }, + expected: { x: -20, y: 0 }, + }, + // too far forward: right + { + change: { x: 70, y: 40 }, + expected: { x: 20, y: 0 }, + }, + // too far forward: bottom + { + change: { x: 20, y: 70 }, + expected: { x: 0, y: 20 }, + }, + + ]; + + items.forEach((item: Item) => { + const result: ?Position = getRemainder({ + current, + max, + change: item.change, + }); + + expect(result).toEqual(item.expected); + }); + }); +}); From e8d36aaa059cdee08aaf8f1c74ed0a768e65896d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 2 Feb 2018 17:24:41 +1100 Subject: [PATCH 048/163] streamlining logic for overflow --- src/state/auto-scroll/can-partially-scroll.js | 108 +++++------ .../auto-scroll/can-partially-scroll.spec.js | 169 ++++++++++++------ 2 files changed, 157 insertions(+), 120 deletions(-) diff --git a/src/state/auto-scroll/can-partially-scroll.js b/src/state/auto-scroll/can-partially-scroll.js index 2917c39c33..f36c3b39c4 100644 --- a/src/state/auto-scroll/can-partially-scroll.js +++ b/src/state/auto-scroll/can-partially-scroll.js @@ -28,35 +28,58 @@ const smallestSigned = apply((value: number) => { return value > 0 ? 1 : -1; }); -const isTooFarBack = (targetScroll: Position): boolean => - targetScroll.x < 0 || targetScroll.y < 0; +type GetRemainderArgs = {| + current: Position, + max: Position, + change: Position, +|} -const isTooFarForward = (targetScroll: Position, maxScroll: Position): boolean => - targetScroll.x > maxScroll.x || targetScroll.y > maxScroll.y; +// We need to figure out how much of the movement +// cannot be done with a scroll +export const getRemainder = (() => { + const getOverlap = (target: number, max: number): number => { + if (target < 0) { + return target; + } + if (target > max) { + return target - max; + } + return 0; + }; + + return ({ + current, + max, + change, + }: GetRemainderArgs): ?Position => { + const targetScroll: Position = add(current, change); + + const remainder: Position = { + x: getOverlap(targetScroll.x, max.x), + y: getOverlap(targetScroll.y, max.y), + }; + + if (isEqual(remainder, origin)) { + return null; + } + + return remainder; + }; +})(); export const canPartiallyScroll = ({ max, current, change, }: CanScrollArgs): boolean => { - // Sure - you can move nowhere if you want - if (isEqual(change, origin)) { - return true; - } - // Only need to be able to move the smallest amount in the desired direction const smallestChange: Position = smallestSigned(change); - const targetScroll: Position = add(current, smallestChange); - - if (isTooFarBack(targetScroll)) { - return false; - } - - if (isTooFarForward(targetScroll, max)) { - return false; - } + const remainder: ?Position = getRemainder({ + max, current, change: smallestChange, + }); - return true; + // there will be no remainder if you can partially scroll + return !remainder; }; const getMaxWindowScroll = (): Position => { @@ -86,8 +109,6 @@ export const canScrollWindow = (change: Position): boolean => { const maxScroll: Position = getMaxWindowScroll(); const currentScroll: Position = getWindowScrollPosition(); - console.warn('can scroll window?'); - return canPartiallyScroll({ current: currentScroll, max: maxScroll, @@ -114,51 +135,6 @@ export const canScrollDroppable = ( }); }; -type GetOverlapArgs = {| - current: Position, - max: Position, - change: Position, -|} - -// We need to figure out how much of the movement -// cannot be done with a scroll -export const getRemainder = ({ - current, - max, - change, -}: GetOverlapArgs): ?Position => { - const canScroll: boolean = canPartiallyScroll({ - current, max, change, - }); - - if (!canScroll) { - return null; - } - - const targetScroll: Position = add(current, change); - - if (isTooFarBack(targetScroll)) { - // if we are moving backwards, any value that is - // positive change be trimmed - const trimmed: Position = { - x: targetScroll.x > 0 ? 0 : targetScroll.x, - y: targetScroll.y > 0 ? 0 : targetScroll.y, - }; - return trimmed; - } - - if (isTooFarForward(targetScroll, max)) { - const trimmed: Position = { - x: targetScroll.x < max.x ? 0 : targetScroll.x - max.x, - y: targetScroll.y < max.y ? 0 : targetScroll.y - max.y, - }; - return trimmed; - } - - // no overlap - return null; -}; - export const getWindowOverlap = (change: Position): ?Position => { if (!canScrollWindow(change)) { return null; diff --git a/test/unit/state/auto-scroll/can-partially-scroll.spec.js b/test/unit/state/auto-scroll/can-partially-scroll.spec.js index 6e0fdde4fd..0d8416bef3 100644 --- a/test/unit/state/auto-scroll/can-partially-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-partially-scroll.spec.js @@ -180,31 +180,8 @@ describe('can partially scroll', () => { }); }); -describe('get overlap', () => { - it('should return null if you cannot partially scroll', () => { - // moving too far back - const current: Position = origin; - const max: Position = origin; - const tooFarBack: Position = { x: 0, y: -1 }; - - const result: ?Position = getRemainder({ - current, max, change: tooFarBack, - }); - - expect(result).toBe(null); - - // validating the result - - const validate: boolean = canPartiallyScroll({ - max, - current, - change: tooFarBack, - }); - - expect(validate).toBe(false); - }); - - it.only('should return the overlap', () => { +describe('get remainder', () => { + describe('returning the remainder', () => { const max: Position = { x: 100, y: 100 }; const current: Position = { x: 50, y: 50 }; @@ -213,38 +190,122 @@ describe('get overlap', () => { expected: Position, |} - const items: Item[] = [ - // too far back: top - { - change: { x: -20, y: -70 }, - expected: { x: 0, y: -20 }, - }, - // too far back: left - { - change: { x: -70, y: -40 }, - expected: { x: -20, y: 0 }, - }, - // too far forward: right - { - change: { x: 70, y: 40 }, - expected: { x: 20, y: 0 }, - }, - // too far forward: bottom - { - change: { x: 20, y: 70 }, - expected: { x: 0, y: 20 }, - }, - - ]; + it('should return overlap on a single axis', () => { + const items: Item[] = [ + // too far back: top + { + change: { x: 0, y: -70 }, + expected: { x: 0, y: -20 }, + }, + // too far back: left + { + change: { x: -70, y: 0 }, + expected: { x: -20, y: 0 }, + }, + // too far forward: right + { + change: { x: 70, y: 0 }, + expected: { x: 20, y: 0 }, + }, + // too far forward: bottom + { + change: { x: 0, y: 70 }, + expected: { x: 0, y: 20 }, + }, + ]; + + items.forEach((item: Item) => { + const result: ?Position = getRemainder({ + current, + max, + change: item.change, + }); + + expect(result).toEqual(item.expected); + }); + }); - items.forEach((item: Item) => { - const result: ?Position = getRemainder({ - current, - max, - change: item.change, + it('should return overlap on two axis in the same direction', () => { + const items: Item[] = [ + // too far back: top + { + change: { x: -80, y: -70 }, + expected: { x: -30, y: -20 }, + }, + // too far back: left + { + change: { x: -70, y: -80 }, + expected: { x: -20, y: -30 }, + }, + // too far forward: right + { + change: { x: 70, y: 0 }, + expected: { x: 20, y: 0 }, + }, + // too far forward: bottom + { + change: { x: 80, y: 70 }, + expected: { x: 30, y: 20 }, + }, + ]; + + items.forEach((item: Item) => { + const result: ?Position = getRemainder({ + current, + max, + change: item.change, + }); + + expect(result).toEqual(item.expected); }); + }); + + it('should return overlap on two axis in different directions', () => { - expect(result).toEqual(item.expected); + }); + + it('should trim values that can be scrolled', () => { + const items: Item[] = [ + // too far back: top + { + // x can be scrolled entirely + // y can be partially scrolled + change: { x: -20, y: -70 }, + expected: { x: 0, y: -20 }, + }, + // too far back: left + { + // x can be partially scrolled + // y can be scrolled entirely + change: { x: -70, y: -40 }, + expected: { x: -20, y: 0 }, + }, + // too far forward: right + { + // x can be partially scrolled + // y can be scrolled entirely + change: { x: 70, y: 40 }, + expected: { x: 20, y: 0 }, + }, + // too far forward: bottom + { + // x can be scrolled entirely + // y can be partially scrolled + change: { x: 20, y: 70 }, + expected: { x: 0, y: 20 }, + }, + + ]; + + items.forEach((item: Item) => { + const result: ?Position = getRemainder({ + current, + max, + change: item.change, + }); + + expect(result).toEqual(item.expected); + }); }); }); }); From 9ee75c73ecd41e5cc8cc4a0e4518d1741d05394a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Sat, 3 Feb 2018 11:13:06 +1100 Subject: [PATCH 049/163] continuing with tests --- ...{can-partially-scroll.js => can-scroll.js} | 4 - src/state/dimension.js | 4 +- ...ally-scroll.spec.js => can-scroll.spec.js} | 103 +++++++++++++++++- test/utils/set-viewport.js | 8 ++ test/utils/set-window-scroll.js | 1 + 5 files changed, 112 insertions(+), 8 deletions(-) rename src/state/auto-scroll/{can-partially-scroll.js => can-scroll.js} (96%) rename test/unit/state/auto-scroll/{can-partially-scroll.spec.js => can-scroll.spec.js} (75%) create mode 100644 test/utils/set-viewport.js diff --git a/src/state/auto-scroll/can-partially-scroll.js b/src/state/auto-scroll/can-scroll.js similarity index 96% rename from src/state/auto-scroll/can-partially-scroll.js rename to src/state/auto-scroll/can-scroll.js index f36c3b39c4..ca9e220d96 100644 --- a/src/state/auto-scroll/can-partially-scroll.js +++ b/src/state/auto-scroll/can-scroll.js @@ -126,8 +126,6 @@ export const canScrollDroppable = ( return false; } - console.warn('can scroll droppable?'); - return canPartiallyScroll({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, @@ -143,7 +141,6 @@ export const getWindowOverlap = (change: Position): ?Position => { const max: Position = getMaxWindowScroll(); const current: Position = getWindowScrollPosition(); - console.warn('getting window overlap'); return getRemainder({ current, max, @@ -162,7 +159,6 @@ export const getDroppableOverlap = (droppable: DroppableDimension, change: Posit return null; } - console.log('getting droppable overlap'); return getRemainder({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, diff --git a/src/state/dimension.js b/src/state/dimension.js index 6a7a89cde8..abfb697a79 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -69,7 +69,7 @@ type GetDroppableArgs = {| descriptor: DroppableDescriptor, client: Area, // optionally provided - and can also be null - closest: ?{| + closest?: {| frameClient: Area, scrollWidth: number, scrollHeight: number, @@ -186,8 +186,6 @@ export const getDroppableDimension = ({ width: frame.width, }); - console.warn('DROPPABLE MAX', maxScroll); - const result: ClosestScrollable = { frame, shouldClipSubject: closest.shouldClipSubject, diff --git a/test/unit/state/auto-scroll/can-partially-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js similarity index 75% rename from test/unit/state/auto-scroll/can-partially-scroll.spec.js rename to test/unit/state/auto-scroll/can-scroll.spec.js index 0d8416bef3..3f0ceb659f 100644 --- a/test/unit/state/auto-scroll/can-partially-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-scroll.spec.js @@ -1,11 +1,21 @@ // @flow import type { Position, + DroppableDimension, } from '../../../../src/types'; -import { canPartiallyScroll, getRemainder } from '../../../../src/state/auto-scroll/can-partially-scroll'; +import { + canPartiallyScroll, + getRemainder, + canScrollDroppable, + canScrollWindow, +} from '../../../../src/state/auto-scroll/can-scroll'; import { add, subtract } from '../../../../src/state/position'; +import getArea from '../../../../src/state/get-area'; +import { getPreset } from '../../../utils/dimension'; +import { getDroppableDimension } from '../../../../src/state/dimension'; const origin: Position = { x: 0, y: 0 }; +const preset = getPreset(); describe('can partially scroll', () => { it('should return true if not scrolling anywhere', () => { @@ -261,7 +271,30 @@ describe('get remainder', () => { }); it('should return overlap on two axis in different directions', () => { + const items: Item[] = [ + // too far back: vertical + // too far forward: horizontal + { + change: { x: 80, y: -70 }, + expected: { x: 30, y: -20 }, + }, + // too far back: horizontal + // too far forward: vertical + { + change: { x: -70, y: 80 }, + expected: { x: -20, y: 30 }, + }, + ]; + + items.forEach((item: Item) => { + const result: ?Position = getRemainder({ + current, + max, + change: item.change, + }); + expect(result).toEqual(item.expected); + }); }); it('should trim values that can be scrolled', () => { @@ -309,3 +342,71 @@ describe('get remainder', () => { }); }); }); + +describe('can scroll droppable', () => { + const scrollable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'drop-1', + type: 'TYPE', + }, + client: getArea({ + top: 0, + left: 0, + right: 100, + bottom: 200, + }), + closest: { + frameClient: getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + scrollWidth: 100, + scrollHeight: 200, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + it('should return false if the droppable is not scrollable', () => { + const result: boolean = canScrollDroppable(preset.home, { x: 1, y: 1 }); + + expect(result).toBe(false); + }); + + it('should return true if the droppable is able to be scrolled', () => { + const result: boolean = canScrollDroppable(scrollable, { x: 0, y: 20 }); + + expect(result).toBe(true); + }); + + it('should return false if the droppable is not able to be scrolled', () => { + const result: boolean = canScrollDroppable(scrollable, { x: -1, y: 0 }); + + expect(result).toBe(false); + }); +}); + +describe('can scroll window', () => { + it('should return true if the window is able to be scrolled', () => { + setViewport(getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }, { x: 0, y: 1 })); + }); + + it('should return false if the window is not able to be scrolled', () => { + + }); +}); + +describe('get droppable remainder', () => { + +}); + +describe('get window remainder', () => { + +}); diff --git a/test/utils/set-viewport.js b/test/utils/set-viewport.js new file mode 100644 index 0000000000..99b2bf29aa --- /dev/null +++ b/test/utils/set-viewport.js @@ -0,0 +1,8 @@ +// @flow + +export default (custom: Area, scroll: Position): void => { + window.pageYOffset = custom.top; + window.pageXOffset = custom.left; + window.innerWidth = custom.width; + window.innerHeight = custom.height; +}; diff --git a/test/utils/set-window-scroll.js b/test/utils/set-window-scroll.js index f8e779ce3d..4b9a5c482a 100644 --- a/test/utils/set-window-scroll.js +++ b/test/utils/set-window-scroll.js @@ -12,6 +12,7 @@ const defaultOptions: Options = { export default (point: Position, options?: Options = defaultOptions) => { window.pageXOffset = point.x; window.pageYOffset = point.y; + if (options.shouldPublish) { window.dispatchEvent(new Event('scroll')); } From ebc98811542d7167aeb1e52f51eaa70af3e15c55 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 5 Feb 2018 08:17:09 +1100 Subject: [PATCH 050/163] moving window functions --- src/state/auto-scroll/can-scroll.js | 8 ++++---- src/state/auto-scroll/create-fluid-scroller.js | 2 +- src/state/auto-scroll/create-jump-scroller.js | 4 ++-- src/state/get-drag-impact/in-foreign-list.js | 2 +- src/state/get-drag-impact/in-home-list.js | 2 +- .../move-cross-axis/get-best-cross-axis-droppable.js | 2 +- src/state/move-cross-axis/get-closest-draggable.js | 2 +- .../move-to-new-droppable/to-foreign-list.js | 2 +- .../move-cross-axis/move-to-new-droppable/to-home-list.js | 2 +- src/state/move-to-next-index/in-foreign-list.js | 2 +- src/state/move-to-next-index/in-home-list.js | 2 +- src/view/drag-drop-context/drag-drop-context.jsx | 4 ++-- .../draggable-dimension-publisher.jsx | 2 +- src/view/draggable/draggable.jsx | 2 +- .../droppable-dimension-publisher.jsx | 2 +- src/{state/visibility => window}/get-viewport.js | 8 ++++---- src/window/get-window-from-ref.js | 3 +++ .../get-window-scroll.js} | 4 +--- test/unit/state/get-drag-impact.spec.js | 2 +- .../move-cross-axis/get-best-cross-axis-droppable.spec.js | 2 +- .../state/move-cross-axis/get-closest-draggable.spec.js | 2 +- test/unit/state/move-cross-axis/move-cross-axis.spec.js | 2 +- .../state/move-cross-axis/move-to-new-droppable.spec.js | 2 +- test/unit/state/move-to-next-index.spec.js | 2 +- 24 files changed, 34 insertions(+), 33 deletions(-) rename src/{state/visibility => window}/get-viewport.js (72%) create mode 100644 src/window/get-window-from-ref.js rename src/{view/get-window-scroll-position.js => window/get-window-scroll.js} (92%) diff --git a/src/state/auto-scroll/can-scroll.js b/src/state/auto-scroll/can-scroll.js index ca9e220d96..1ffbbe6c16 100644 --- a/src/state/auto-scroll/can-scroll.js +++ b/src/state/auto-scroll/can-scroll.js @@ -1,8 +1,8 @@ // @flow import { add, apply, isEqual, subtract } from '../position'; // TODO: state reaching into VIEW :( -import getWindowScrollPosition from '../../view/get-window-scroll-position'; -import getViewport from '../visibility/get-viewport'; +import getWindowScroll from '../../window/get-window-scroll'; +import getViewport from '../../window/get-viewport'; import getMaxScroll from '../get-max-scroll'; import type { ClosestScrollable, @@ -107,7 +107,7 @@ const getMaxWindowScroll = (): Position => { export const canScrollWindow = (change: Position): boolean => { const maxScroll: Position = getMaxWindowScroll(); - const currentScroll: Position = getWindowScrollPosition(); + const currentScroll: Position = getWindowScroll(); return canPartiallyScroll({ current: currentScroll, @@ -139,7 +139,7 @@ export const getWindowOverlap = (change: Position): ?Position => { } const max: Position = getMaxWindowScroll(); - const current: Position = getWindowScrollPosition(); + const current: Position = getWindowScroll(); return getRemainder({ current, diff --git a/src/state/auto-scroll/create-fluid-scroller.js b/src/state/auto-scroll/create-fluid-scroller.js index 3365cb6cdd..b4cb887a3e 100644 --- a/src/state/auto-scroll/create-fluid-scroller.js +++ b/src/state/auto-scroll/create-fluid-scroller.js @@ -1,6 +1,6 @@ // @flow import rafSchd from 'raf-schd'; -import getViewport from '../visibility/get-viewport'; +import getViewport from '../../window/get-viewport'; import { isEqual } from '../position'; import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; import getScrollableDroppableOver from './get-scrollable-droppable-over'; diff --git a/src/state/auto-scroll/create-jump-scroller.js b/src/state/auto-scroll/create-jump-scroller.js index b190bbe8b6..e1a00b9be2 100644 --- a/src/state/auto-scroll/create-jump-scroller.js +++ b/src/state/auto-scroll/create-jump-scroller.js @@ -1,8 +1,8 @@ // @flow import { add } from '../position'; -import getWindowScrollPosition from '../../view/get-window-scroll-position'; +import getWindowScrollPosition from '../../window/get-window-scroll'; import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; -import getViewport from '../visibility/get-viewport'; +import getViewport from '../../window/get-viewport'; import { move as moveAction } from '../action-creators'; import { canScrollDroppable, diff --git a/src/state/get-drag-impact/in-foreign-list.js b/src/state/get-drag-impact/in-foreign-list.js index dafe6b608f..7144a5d7df 100644 --- a/src/state/get-drag-impact/in-foreign-list.js +++ b/src/state/get-drag-impact/in-foreign-list.js @@ -11,7 +11,7 @@ import type { } from '../../types'; import { add, patch } from '../position'; import getDisplacement from '../get-displacement'; -import getViewport from '../visibility/get-viewport'; +import getViewport from '../../window/get-viewport'; type Args = {| pageCenter: Position, diff --git a/src/state/get-drag-impact/in-home-list.js b/src/state/get-drag-impact/in-home-list.js index 49cd3b37a6..8cd6b0c290 100644 --- a/src/state/get-drag-impact/in-home-list.js +++ b/src/state/get-drag-impact/in-home-list.js @@ -11,7 +11,7 @@ import type { } from '../../types'; import { add, patch } from '../position'; import getDisplacement from '../get-displacement'; -import getViewport from '../visibility/get-viewport'; +import getViewport from '../../window/get-viewport'; // It is the responsibility of this function // to return the impact of a drag diff --git a/src/state/move-cross-axis/get-best-cross-axis-droppable.js b/src/state/move-cross-axis/get-best-cross-axis-droppable.js index 868abefdc5..7abc23da71 100644 --- a/src/state/move-cross-axis/get-best-cross-axis-droppable.js +++ b/src/state/move-cross-axis/get-best-cross-axis-droppable.js @@ -2,7 +2,7 @@ import { closest } from '../position'; import isWithin from '../is-within'; import { getCorners } from '../spacing'; -import getViewport from '../visibility/get-viewport'; +import getViewport from '../../window/get-viewport'; import isPartiallyVisibleThroughFrame from '../visibility/is-partially-visible-through-frame'; import type { Axis, diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index 404faaea1d..a078faf81f 100644 --- a/src/state/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-cross-axis/get-closest-draggable.js @@ -1,6 +1,6 @@ // @flow import { add, distance } from '../position'; -import getViewport from '../visibility/get-viewport'; +import getViewport from '../../window/get-viewport'; import { isTotallyVisible } from '../visibility/is-visible'; import type { Area, diff --git a/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js index c21ed9251f..318eaa570a 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js +++ b/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js @@ -2,7 +2,7 @@ import moveToEdge from '../../move-to-edge'; import type { Result } from '../move-cross-axis-types'; import getDisplacement from '../../get-displacement'; -import getViewport from '../../visibility/get-viewport'; +import getViewport from '../../../window/get-viewport'; import { add } from '../../position'; import type { Axis, diff --git a/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js index 255c7619fc..4ec809d17c 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js +++ b/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js @@ -1,6 +1,6 @@ // @flow import moveToEdge from '../../move-to-edge'; -import getViewport from '../../visibility/get-viewport'; +import getViewport from '../../../window/get-viewport'; import getDisplacement from '../../get-displacement'; import withDroppableScroll from '../../with-droppable-scroll'; import type { Edge } from '../../move-to-edge'; diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index fe0d494446..5486c3a1e1 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -4,7 +4,7 @@ import { patch } from '../position'; import withDroppableScroll from '../with-droppable-scroll'; import moveToEdge from '../move-to-edge'; import getDisplacement from '../get-displacement'; -import getViewport from '../visibility/get-viewport'; +import getViewport from '../../window/get-viewport'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import getScrollJumpResult from './get-scroll-jump-result'; import type { Edge } from '../move-to-edge'; diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 939e57d573..307fa59914 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -3,7 +3,7 @@ import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import { patch } from '../position'; import withDroppableScroll from '../with-droppable-scroll'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; -import getViewport from '../visibility/get-viewport'; +import getViewport from '../../window/get-viewport'; import getScrollJumpResult from './get-scroll-jump-result'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 2d652a82cc..e2ace94f04 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -6,8 +6,8 @@ import fireHooks from '../../state/fire-hooks'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; import createStyleMarshal from '../style-marshal/style-marshal'; import canStartDrag from '../../state/can-start-drag'; -import createAutoScroll from '../../state/auto-scroll-marshal/auto-scroll-marshal'; -import type { AutoScrollMarshal } from '../../state/auto-scroll-marshal/auto-scroll-marshal-types'; +import createAutoScroll from '../../state/auto-scroll/auto-scroll-marshal'; +import type { AutoScrollMarshal } from '../../state/auto-scroll/auto-scroll-marshal-types'; import type { StyleMarshal } from '../style-marshal/style-marshal-types'; import type { DimensionMarshal, diff --git a/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx b/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx index 481afca400..d25e2dd410 100644 --- a/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx +++ b/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx @@ -3,7 +3,7 @@ import { Component } from 'react'; import type { Node } from 'react'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; -import getWindowScrollPosition from '../get-window-scroll-position'; +import getWindowScrollPosition from '../../window/get-window-scroll'; import { getDraggableDimension } from '../../state/dimension'; import { dimensionMarshalKey } from '../context-keys'; import getArea from '../../state/get-area'; diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index b75bbb7914..4004d07305 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -13,7 +13,7 @@ import type { import DraggableDimensionPublisher from '../draggable-dimension-publisher/'; import Moveable from '../moveable/'; import DragHandle from '../drag-handle'; -import getWindowScrollPosition from '../get-window-scroll-position'; +import getWindowScrollPosition from '../../window/get-window-scroll'; // eslint-disable-next-line no-duplicate-imports import type { DragHandleProps, diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 8ae48b37af..3bd833b8a6 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -4,7 +4,7 @@ import type { Node } from 'react'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import rafSchedule from 'raf-schd'; -import getWindowScrollPosition from '../get-window-scroll-position'; +import getWindowScrollPosition from '../../window/get-window-scroll'; import getArea from '../../state/get-area'; import { getDroppableDimension } from '../../state/dimension'; import getClosestScrollable from '../get-closest-scrollable'; diff --git a/src/state/visibility/get-viewport.js b/src/window/get-viewport.js similarity index 72% rename from src/state/visibility/get-viewport.js rename to src/window/get-viewport.js index df9785a65a..03fb08cc28 100644 --- a/src/state/visibility/get-viewport.js +++ b/src/window/get-viewport.js @@ -1,10 +1,10 @@ // @flow -import type { Position, Area } from '../../types'; -import getArea from '../get-area'; -import getWindowScrollPosition from '../../view/get-window-scroll-position'; +import type { Position, Area } from '../types'; +import getArea from '../state/get-area'; +import getWindowScroll from './get-window-scroll'; export default (): Area => { - const windowScroll: Position = getWindowScrollPosition(); + const windowScroll: Position = getWindowScroll(); const top: number = windowScroll.y; const left: number = windowScroll.x; diff --git a/src/window/get-window-from-ref.js b/src/window/get-window-from-ref.js new file mode 100644 index 0000000000..4fa01fc348 --- /dev/null +++ b/src/window/get-window-from-ref.js @@ -0,0 +1,3 @@ +// @flow +export default (ref: ?HTMLElement): HTMLElement => + (ref ? ref.ownerDocument.defaultView : window); diff --git a/src/view/get-window-scroll-position.js b/src/window/get-window-scroll.js similarity index 92% rename from src/view/get-window-scroll-position.js rename to src/window/get-window-scroll.js index fa4dcf6576..abec8422c2 100644 --- a/src/view/get-window-scroll-position.js +++ b/src/window/get-window-scroll.js @@ -2,8 +2,6 @@ import { apply } from '../state/position'; import type { Position } from '../types'; -const floor = apply(Math.floor); - // The browsers update document.documentElement.scrollTop and window.pageYOffset // differently as the window scrolls. @@ -27,7 +25,7 @@ const floor = apply(Math.floor); // documentElement.scrollTop: no update. Stays at 0 // window.pageYOffset: updates to whole number -export default (): Position => floor({ +export default (): Position => ({ x: window.pageXOffset, y: window.pageYOffset, }); diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index 24a9b7beb4..ec222beea1 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -14,7 +14,7 @@ import { getPreset, disableDroppable, } from '../../utils/dimension'; -import getViewport from '../../../src/state/visibility/get-viewport'; +import getViewport from '../../../src/window/get-viewport'; import type { Axis, DraggableDimension, diff --git a/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js b/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js index fb12a2803e..44b04fd93a 100644 --- a/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js +++ b/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js @@ -4,7 +4,7 @@ import { getDroppableDimension } from '../../../../src/state/dimension'; import getArea from '../../../../src/state/get-area'; import { add } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; -import getViewport from '../../../../src/state/visibility/get-viewport'; +import getViewport from '../../../../src/window/get-viewport'; import type { Axis, Position, diff --git a/test/unit/state/move-cross-axis/get-closest-draggable.spec.js b/test/unit/state/move-cross-axis/get-closest-draggable.spec.js index c2680f1f04..991e862055 100644 --- a/test/unit/state/move-cross-axis/get-closest-draggable.spec.js +++ b/test/unit/state/move-cross-axis/get-closest-draggable.spec.js @@ -4,7 +4,7 @@ import { getDroppableDimension, getDraggableDimension } from '../../../../src/st import { add, distance, patch } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; import getArea from '../../../../src/state/get-area'; -import getViewport from '../../../../src/state/visibility/get-viewport'; +import getViewport from '../../../../src/window/get-viewport'; import type { Axis, Position, diff --git a/test/unit/state/move-cross-axis/move-cross-axis.spec.js b/test/unit/state/move-cross-axis/move-cross-axis.spec.js index 2aecfa77d9..72856c2722 100644 --- a/test/unit/state/move-cross-axis/move-cross-axis.spec.js +++ b/test/unit/state/move-cross-axis/move-cross-axis.spec.js @@ -1,7 +1,7 @@ // @flow import moveCrossAxis from '../../../../src/state/move-cross-axis/'; import noImpact from '../../../../src/state/no-impact'; -import getViewport from '../../../../src/state/visibility/get-viewport'; +import getViewport from '../../../../src/window/get-viewport'; import getArea from '../../../../src/state/get-area'; import { getDroppableDimension, getDraggableDimension } from '../../../../src/state/dimension'; import { getPreset } from '../../../utils/dimension'; diff --git a/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js b/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js index 6995d0fa07..e1ce3466b1 100644 --- a/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js +++ b/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js @@ -8,7 +8,7 @@ import { patch } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; import { getPreset } from '../../../utils/dimension'; import noImpact from '../../../../src/state/no-impact'; -import getViewport from '../../../../src/state/visibility/get-viewport'; +import getViewport from '../../../../src/window/get-viewport'; import type { Axis, DragImpact, diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index 7758012a3a..8b44dff65e 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -6,7 +6,7 @@ import moveToEdge from '../../../src/state/move-to-edge'; import noImpact, { noMovement } from '../../../src/state/no-impact'; import { patch } from '../../../src/state/position'; import { vertical, horizontal } from '../../../src/state/axis'; -import getViewport from '../../../src/state/visibility/get-viewport'; +import getViewport from '../../../src/window/get-viewport'; import getArea from '../../../src/state/get-area'; import setWindowScroll from '../../utils/set-window-scroll'; import { getDroppableDimension, getDraggableDimension } from '../../../src/state/dimension'; From 6b6b1127293668781126b55dcb632c54c7ef8d50 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 5 Feb 2018 10:27:57 +1100 Subject: [PATCH 051/163] finialising can-scroll tests --- src/state/auto-scroll/can-scroll.js | 41 +- test/setup.js | 11 + .../unit/state/auto-scroll/can-scroll.spec.js | 486 ++++++++++++------ test/utils/set-viewport.js | 17 +- test/utils/set-window-scroll-size.js | 34 ++ test/utils/set-window-scroll.js | 11 +- 6 files changed, 426 insertions(+), 174 deletions(-) create mode 100644 test/utils/set-window-scroll-size.js diff --git a/src/state/auto-scroll/can-scroll.js b/src/state/auto-scroll/can-scroll.js index 1ffbbe6c16..23512b4acf 100644 --- a/src/state/auto-scroll/can-scroll.js +++ b/src/state/auto-scroll/can-scroll.js @@ -36,8 +36,12 @@ type GetRemainderArgs = {| // We need to figure out how much of the movement // cannot be done with a scroll -export const getRemainder = (() => { - const getOverlap = (target: number, max: number): number => { +export const getOverlap = (() => { + const getRemainder = (target: number, max: number): number => { + if (target === 0) { + return 0; + } + if (target < 0) { return target; } @@ -54,16 +58,23 @@ export const getRemainder = (() => { }: GetRemainderArgs): ?Position => { const targetScroll: Position = add(current, change); - const remainder: Position = { - x: getOverlap(targetScroll.x, max.x), - y: getOverlap(targetScroll.y, max.y), + console.log('target', targetScroll); + console.log('max', max); + console.log('x', getRemainder(targetScroll.x, max.x)); + console.log('y', getRemainder(targetScroll.y, max.y)); + + const overlap: Position = { + x: getRemainder(targetScroll.x, max.x), + y: getRemainder(targetScroll.y, max.y), }; - if (isEqual(remainder, origin)) { + console.log('overlap', overlap); + + if (isEqual(overlap, origin)) { return null; } - return remainder; + return overlap; }; })(); @@ -74,12 +85,14 @@ export const canPartiallyScroll = ({ }: CanScrollArgs): boolean => { // Only need to be able to move the smallest amount in the desired direction const smallestChange: Position = smallestSigned(change); - const remainder: ?Position = getRemainder({ + console.log('smallest change', smallestChange); + + const overlap: ?Position = getOverlap({ max, current, change: smallestChange, }); // there will be no remainder if you can partially scroll - return !remainder; + return !overlap; }; const getMaxWindowScroll = (): Position => { @@ -123,9 +136,14 @@ export const canScrollDroppable = ( const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; // Cannot scroll when there is no scroll container! if (!closestScrollable) { + console.log('no closest scrollable'); return false; } + console.log('closestScrollable min', closestScrollable.scroll.current); + console.log('closestScrollable max', closestScrollable.scroll.max); + console.log('change', change); + return canPartiallyScroll({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, @@ -141,7 +159,7 @@ export const getWindowOverlap = (change: Position): ?Position => { const max: Position = getMaxWindowScroll(); const current: Position = getWindowScroll(); - return getRemainder({ + return getOverlap({ current, max, change, @@ -150,6 +168,7 @@ export const getWindowOverlap = (change: Position): ?Position => { export const getDroppableOverlap = (droppable: DroppableDimension, change: Position): ?Position => { if (!canScrollDroppable(droppable, change)) { + console.log('cannot scroll droppable'); return null; } @@ -159,7 +178,7 @@ export const getDroppableOverlap = (droppable: DroppableDimension, change: Posit return null; } - return getRemainder({ + return getOverlap({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, change, diff --git a/test/setup.js b/test/setup.js index 0b55eafc88..b44902885c 100644 --- a/test/setup.js +++ b/test/setup.js @@ -6,6 +6,16 @@ // run with browser globals enabled if (typeof window !== 'undefined') { require('raf-stub').replaceRaf([global, window]); + + // overriding these properties in jsdom to allow them to be controlled + + Object.defineProperty(document.documentElement, 'scrollHeight', { + writable: true, + }); + + Object.defineProperty(document.documentElement, 'scrollWidth', { + writable: true, + }); } // setting up global enzyme @@ -14,3 +24,4 @@ const Enzyme = require('enzyme'); const Adapter = require('enzyme-adapter-react-15'); Enzyme.configure({ adapter: new Adapter() }); + diff --git a/test/unit/state/auto-scroll/can-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js index 3f0ceb659f..657e5066ab 100644 --- a/test/unit/state/auto-scroll/can-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-scroll.spec.js @@ -5,112 +5,154 @@ import type { } from '../../../../src/types'; import { canPartiallyScroll, - getRemainder, + getOverlap, + getWindowOverlap, + getDroppableOverlap, canScrollDroppable, canScrollWindow, } from '../../../../src/state/auto-scroll/can-scroll'; import { add, subtract } from '../../../../src/state/position'; import getArea from '../../../../src/state/get-area'; import { getPreset } from '../../../utils/dimension'; -import { getDroppableDimension } from '../../../../src/state/dimension'; +import { getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; +import setViewport, { resetViewport } from '../../../utils/set-viewport'; +import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; +import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; const origin: Position = { x: 0, y: 0 }; const preset = getPreset(); -describe('can partially scroll', () => { - it('should return true if not scrolling anywhere', () => { - const result: boolean = canPartiallyScroll({ - max: { x: 100, y: 100 }, - current: { x: 0, y: 0 }, - // not - change: origin, - }); +const scrollableScrollSize = { + scrollWidth: 200, + scrollHeight: 200, +}; + +const scrollable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'drop-1', + type: 'TYPE', + }, + client: getArea({ + top: 0, + left: 0, + right: 100, + bottom: 200, + }), + closest: { + frameClient: getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + scrollWidth: scrollableScrollSize.scrollWidth, + scrollHeight: scrollableScrollSize.scrollHeight, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, +}); - expect(result).toBe(true); +describe('can scroll', () => { + afterEach(() => { + resetViewport(); + resetWindowScroll(); + resetWindowScrollSize(); }); - it('should return true if scrolling to a boundary', () => { - const current: Position = origin; - const max: Position = { x: 100, y: 200 }; - - const corners: Position[] = [ - // top left - { x: 0, y: 0 }, - // top right - { x: max.x, y: 0 }, - // bottom right - { x: max.x, y: max.y }, - // bottom left - { x: 0, y: max.y }, - ]; - - corners.forEach((corner: Position) => { + describe('can partially scroll', () => { + it('should return true if not scrolling anywhere', () => { const result: boolean = canPartiallyScroll({ - max, - current, - change: corner, + max: { x: 100, y: 100 }, + current: { x: 0, y: 0 }, + // not + change: origin, }); expect(result).toBe(true); }); - }); - it('should return true if moving in any direction within the allowable scroll region', () => { - const max: Position = { x: 100, y: 100 }; - const current: Position = { x: 50, y: 50 }; + it('should return true if scrolling to a boundary', () => { + const current: Position = origin; + const max: Position = { x: 100, y: 200 }; - // all of these movements are totally possible - const changes: Position[] = [ + const corners: Position[] = [ // top left - { x: -10, y: 10 }, - // top right - { x: 10, y: 10 }, - // bottom right - { x: 10, y: -10 }, - // bottom left - { x: -10, y: -10 }, - ]; + { x: 0, y: 0 }, + // top right + { x: max.x, y: 0 }, + // bottom right + { x: max.x, y: max.y }, + // bottom left + { x: 0, y: max.y }, + ]; - changes.forEach((point: Position) => { - const result: boolean = canPartiallyScroll({ - max, - current, - change: point, - }); + corners.forEach((corner: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: corner, + }); - expect(result).toBe(true); + expect(result).toBe(true); + }); }); - }); - it('should return true if able to partially move in both directions', () => { - const max: Position = { x: 100, y: 100 }; - const current: Position = { x: 50, y: 50 }; + it('should return true if moving in any direction within the allowable scroll region', () => { + const max: Position = { x: 100, y: 100 }; + const current: Position = { x: 50, y: 50 }; - // all of these movements are partially possible - const changes: Position[] = [ + // all of these movements are totally possible + const changes: Position[] = [ // top left - { x: -200, y: 200 }, - // top right - { x: 200, y: 200 }, - // bottom right - { x: 200, y: -200 }, - // bottom left - { x: -200, y: -200 }, - ]; + { x: -10, y: 10 }, + // top right + { x: 10, y: 10 }, + // bottom right + { x: 10, y: -10 }, + // bottom left + { x: -10, y: -10 }, + ]; - changes.forEach((point: Position) => { - const result: boolean = canPartiallyScroll({ - max, - current, - change: point, + changes.forEach((point: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: point, + }); + + expect(result).toBe(true); }); + }); - expect(result).toBe(true); + it('should return true if able to partially move in both directions', () => { + const max: Position = { x: 100, y: 100 }; + const current: Position = { x: 50, y: 50 }; + + // all of these movements are partially possible + const changes: Position[] = [ + // top left + { x: -200, y: 200 }, + // top right + { x: 200, y: 200 }, + // bottom right + { x: 200, y: -200 }, + // bottom left + { x: -200, y: -200 }, + ]; + + changes.forEach((point: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: point, + }); + + expect(result).toBe(true); + }); }); - }); - it('should return false if can only scroll in one direction', () => { - const max: Position = { x: 100, y: 200 }; + it('should return false if can only scroll in one direction', () => { + const max: Position = { x: 100, y: 200 }; type Item = {| current: Position, @@ -149,51 +191,51 @@ describe('can partially scroll', () => { expect(result).toBe(false); }); - }); + }); - it('should return false if on the min point and move backward in any direction', () => { - const current: Position = origin; - const max: Position = { x: 100, y: 200 }; - const tooFarBack: Position[] = [ - { x: 0, y: -1 }, - { x: -1, y: 0 }, - ]; + it('should return false if on the min point and move backward in any direction', () => { + const current: Position = origin; + const max: Position = { x: 100, y: 200 }; + const tooFarBack: Position[] = [ + { x: 0, y: -1 }, + { x: -1, y: 0 }, + ]; - tooFarBack.forEach((point: Position) => { - const result: boolean = canPartiallyScroll({ - max, - current, - change: point, - }); + tooFarBack.forEach((point: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: point, + }); - expect(result).toBe(false); + expect(result).toBe(false); + }); }); - }); - it('should return false if on the max point and move forward in any direction', () => { - const max: Position = { x: 100, y: 200 }; - const current: Position = max; - const tooFarForward: Position[] = [ - add(max, { x: 0, y: 1 }), - add(max, { x: 1, y: 0 }), - ]; + it('should return false if on the max point and move forward in any direction', () => { + const max: Position = { x: 100, y: 200 }; + const current: Position = max; + const tooFarForward: Position[] = [ + add(max, { x: 0, y: 1 }), + add(max, { x: 1, y: 0 }), + ]; - tooFarForward.forEach((point: Position) => { - const result: boolean = canPartiallyScroll({ - max, - current, - change: point, - }); + tooFarForward.forEach((point: Position) => { + const result: boolean = canPartiallyScroll({ + max, + current, + change: point, + }); - expect(result).toBe(false); + expect(result).toBe(false); + }); }); }); -}); -describe('get remainder', () => { - describe('returning the remainder', () => { - const max: Position = { x: 100, y: 100 }; - const current: Position = { x: 50, y: 50 }; + describe('get overlap', () => { + describe('returning the remainder', () => { + const max: Position = { x: 100, y: 100 }; + const current: Position = { x: 50, y: 50 }; type Item = {| change: Position, @@ -225,7 +267,7 @@ describe('get remainder', () => { ]; items.forEach((item: Item) => { - const result: ?Position = getRemainder({ + const result: ?Position = getOverlap({ current, max, change: item.change, @@ -260,7 +302,7 @@ describe('get remainder', () => { ]; items.forEach((item: Item) => { - const result: ?Position = getRemainder({ + const result: ?Position = getOverlap({ current, max, change: item.change, @@ -287,7 +329,7 @@ describe('get remainder', () => { ]; items.forEach((item: Item) => { - const result: ?Position = getRemainder({ + const result: ?Position = getOverlap({ current, max, change: item.change, @@ -331,7 +373,7 @@ describe('get remainder', () => { ]; items.forEach((item: Item) => { - const result: ?Position = getRemainder({ + const result: ?Position = getOverlap({ current, max, change: item.change, @@ -340,73 +382,195 @@ describe('get remainder', () => { expect(result).toEqual(item.expected); }); }); + }); }); -}); -describe('can scroll droppable', () => { - const scrollable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'drop-1', - type: 'TYPE', - }, - client: getArea({ - top: 0, - left: 0, - right: 100, - bottom: 200, - }), - closest: { - frameClient: getArea({ + describe('can scroll droppable', () => { + it('should return false if the droppable is not scrollable', () => { + const result: boolean = canScrollDroppable(preset.home, { x: 1, y: 1 }); + + expect(result).toBe(false); + }); + + it('should return true if the droppable is able to be scrolled', () => { + const result: boolean = canScrollDroppable(scrollable, { x: 0, y: 20 }); + + expect(result).toBe(true); + }); + + it('should return false if the droppable is not able to be scrolled', () => { + const result: boolean = canScrollDroppable(scrollable, { x: -1, y: 0 }); + + expect(result).toBe(false); + }); + }); + + describe('can scroll window', () => { + it('should return true if the window is able to be scrolled', () => { + setViewport(getArea({ top: 0, left: 0, right: 100, bottom: 100, - }), - scrollWidth: 100, - scrollHeight: 200, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); + })); + setWindowScrollSize({ + scrollHeight: 200, + scrollWidth: 100, + }); + setWindowScroll(origin); - it('should return false if the droppable is not scrollable', () => { - const result: boolean = canScrollDroppable(preset.home, { x: 1, y: 1 }); + const result: boolean = canScrollWindow({ x: 0, y: 50 }); - expect(result).toBe(false); - }); + expect(result).toBe(true); + }); - it('should return true if the droppable is able to be scrolled', () => { - const result: boolean = canScrollDroppable(scrollable, { x: 0, y: 20 }); + it('should return false if the window is not able to be scrolled', () => { + setViewport(getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + })); + setWindowScrollSize({ + scrollHeight: 200, + scrollWidth: 100, + }); + // already at the max scroll + setWindowScroll({ + x: 0, + y: 200, + }); + + const result: boolean = canScrollWindow({ x: 0, y: 1 }); - expect(result).toBe(true); + expect(result).toBe(false); + }); }); - it('should return false if the droppable is not able to be scrolled', () => { - const result: boolean = canScrollDroppable(scrollable, { x: -1, y: 0 }); + describe('get droppable overlap', () => { + it('should return null if there is no scroll container', () => { + const result: ?Position = getDroppableOverlap(preset.home, { x: 1, y: 1 }); - expect(result).toBe(false); - }); -}); + expect(result).toBe(null); + }); -describe('can scroll window', () => { - it('should return true if the window is able to be scrolled', () => { - setViewport(getArea({ - top: 0, - left: 0, - right: 100, - bottom: 100, - }, { x: 0, y: 1 })); - }); + it('should return null if the droppable cannot be scrolled', () => { + // end of the scrollable area + const scroll: Position = { + x: 0, + y: 200, + }; + const scrolled: DroppableDimension = scrollDroppable(scrollable, scroll); + const result: ?Position = getDroppableOverlap(scrolled, { x: 0, y: 1 }); + + expect(result).toBe(null); + }); + + // tested in get remainder + it.only('should return the overlap', () => { + // how far the droppable has already + const scroll: Position = { + x: 10, y: 20, + }; + const scrolled: DroppableDimension = scrollDroppable(scrollable, scroll); + // $ExpectError - not checking for null + const max: Position = scrolled.viewport.closestScrollable.scroll.max; + const totalSpace: Position = { + x: scrollableScrollSize.scrollWidth - max.x, + y: scrollableScrollSize.scrollHeight - max.y, + }; + const remainingSpace = subtract(totalSpace, scroll); + const change: Position = { x: 300, y: 300 }; + const expectedOverlap: Position = subtract(change, remainingSpace); + + const result: ?Position = getDroppableOverlap(scrolled, change); + + expect(result).toEqual(expectedOverlap); + }); + + it('should return null if there is no overlap', () => { + const change: Position = { x: 0, y: 1 }; - it('should return false if the window is not able to be scrolled', () => { + const result: ?Position = getDroppableOverlap(scrollable, change); + expect(result).toEqual(null); + + // verifying correctness of test + + expect(canScrollDroppable(scrollable, change)).toBe(true); + }); }); -}); -describe('get droppable remainder', () => { + describe('get window overlap', () => { + it('should return null if the window cannot be scrolled', () => { + setViewport(getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + })); + setWindowScrollSize({ + scrollHeight: 200, + scrollWidth: 100, + }); + // already at the max scroll + setWindowScroll({ + x: 0, + y: 200, + }); -}); + const result: ?Position = getWindowOverlap({ x: 0, y: 1 }); -describe('get window remainder', () => { + expect(result).toBe(null); + }); + + // tested in get remainder + it('should return the overlap', () => { + setViewport(getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + })); + const windowScrollSize = { + scrollHeight: 200, + scrollWidth: 100, + }; + setWindowScrollSize(windowScrollSize); + const windowScroll: Position = { + x: 50, + y: 50, + }; + setWindowScroll(windowScroll); + const change: Position = { x: 300, y: 300 }; + const space: Position = { + x: windowScrollSize.scrollWidth - windowScroll.x, + y: windowScrollSize.scrollHeight - windowScroll.y, + }; + const overlap: Position = subtract(change, space); + + const result: ?Position = getWindowOverlap(change); + + expect(result).toEqual(overlap); + }); + it('should return null if there is no overlap', () => { + setViewport(getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + })); + const scrollSize = { + scrollHeight: 200, + scrollWidth: 100, + }; + setWindowScrollSize(scrollSize); + setWindowScroll(origin); + + const result: ?Position = getWindowOverlap({ x: 10, y: 10 }); + + expect(result).toBe(null); + }); + }); }); diff --git a/test/utils/set-viewport.js b/test/utils/set-viewport.js index 99b2bf29aa..c299989a6b 100644 --- a/test/utils/set-viewport.js +++ b/test/utils/set-viewport.js @@ -1,8 +1,23 @@ // @flow +import type { Area } from '../../src/types'; +import getArea from '../../src/state/get-area'; -export default (custom: Area, scroll: Position): void => { +const setViewport = (custom: Area) => { window.pageYOffset = custom.top; window.pageXOffset = custom.left; window.innerWidth = custom.width; window.innerHeight = custom.height; }; + +export const getCurrent = (): Area => getArea({ + top: window.pageYOffset, + left: window.pageXOffset, + width: window.innerWidth, + height: window.innerHeight, +}); + +const original: Area = getCurrent(); + +export const resetViewport = () => setViewport(original); + +export default setViewport; diff --git a/test/utils/set-window-scroll-size.js b/test/utils/set-window-scroll-size.js new file mode 100644 index 0000000000..624feca4ae --- /dev/null +++ b/test/utils/set-window-scroll-size.js @@ -0,0 +1,34 @@ +// @flow + +type Args = {| + scrollHeight: number, + scrollWidth: number, +|} + +const setWindowScrollSize = ({ scrollHeight, scrollWidth }: Args): void => { + const el: ?HTMLElement = document.documentElement; + + if (!el) { + throw new Error('Unable to find document element'); + } + + el.scrollHeight = scrollHeight; + el.scrollWidth = scrollWidth; +}; + +const original: Args = (() => { + const el: ?HTMLElement = document.documentElement; + + if (!el) { + throw new Error('Unable to find document element'); + } + + return { + scrollWidth: el.scrollWidth, + scrollHeight: el.scrollHeight, + }; +})(); + +export const resetWindowScrollSize = () => setWindowScrollSize(original); + +export default setWindowScrollSize; diff --git a/test/utils/set-window-scroll.js b/test/utils/set-window-scroll.js index 4b9a5c482a..332af8050c 100644 --- a/test/utils/set-window-scroll.js +++ b/test/utils/set-window-scroll.js @@ -9,7 +9,7 @@ const defaultOptions: Options = { shouldPublish: true, }; -export default (point: Position, options?: Options = defaultOptions) => { +const setWindowScroll = (point: Position, options?: Options = defaultOptions) => { window.pageXOffset = point.x; window.pageYOffset = point.y; @@ -17,3 +17,12 @@ export default (point: Position, options?: Options = defaultOptions) => { window.dispatchEvent(new Event('scroll')); } }; + +const original: Position = { + x: window.pageXOffset, + y: window.pageYOffset, +}; + +export const resetWindowScroll = () => setWindowScroll(original); + +export default setWindowScroll; From c355628aac70c542cfd463400ca175dfbdf88152 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 5 Feb 2018 10:28:42 +1100 Subject: [PATCH 052/163] cleaning up can scroll file --- src/state/auto-scroll/can-scroll.js | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/state/auto-scroll/can-scroll.js b/src/state/auto-scroll/can-scroll.js index 23512b4acf..d2bc504607 100644 --- a/src/state/auto-scroll/can-scroll.js +++ b/src/state/auto-scroll/can-scroll.js @@ -1,5 +1,5 @@ // @flow -import { add, apply, isEqual, subtract } from '../position'; +import { add, apply, isEqual } from '../position'; // TODO: state reaching into VIEW :( import getWindowScroll from '../../window/get-window-scroll'; import getViewport from '../../window/get-viewport'; @@ -7,7 +7,6 @@ import getMaxScroll from '../get-max-scroll'; import type { ClosestScrollable, DroppableDimension, - Spacing, Position, Area, } from '../../types'; @@ -58,18 +57,11 @@ export const getOverlap = (() => { }: GetRemainderArgs): ?Position => { const targetScroll: Position = add(current, change); - console.log('target', targetScroll); - console.log('max', max); - console.log('x', getRemainder(targetScroll.x, max.x)); - console.log('y', getRemainder(targetScroll.y, max.y)); - const overlap: Position = { x: getRemainder(targetScroll.x, max.x), y: getRemainder(targetScroll.y, max.y), }; - console.log('overlap', overlap); - if (isEqual(overlap, origin)) { return null; } @@ -85,7 +77,6 @@ export const canPartiallyScroll = ({ }: CanScrollArgs): boolean => { // Only need to be able to move the smallest amount in the desired direction const smallestChange: Position = smallestSigned(change); - console.log('smallest change', smallestChange); const overlap: ?Position = getOverlap({ max, current, change: smallestChange, @@ -99,7 +90,6 @@ const getMaxWindowScroll = (): Position => { const el: ?HTMLElement = document.documentElement; if (!el) { - console.error('Cannot find document element'); return origin; } @@ -136,14 +126,9 @@ export const canScrollDroppable = ( const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; // Cannot scroll when there is no scroll container! if (!closestScrollable) { - console.log('no closest scrollable'); return false; } - console.log('closestScrollable min', closestScrollable.scroll.current); - console.log('closestScrollable max', closestScrollable.scroll.max); - console.log('change', change); - return canPartiallyScroll({ current: closestScrollable.scroll.current, max: closestScrollable.scroll.max, @@ -168,7 +153,6 @@ export const getWindowOverlap = (change: Position): ?Position => { export const getDroppableOverlap = (droppable: DroppableDimension, change: Position): ?Position => { if (!canScrollDroppable(droppable, change)) { - console.log('cannot scroll droppable'); return null; } From 015b7ab228e77902287e4748bc2ceb8df98e8163 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 5 Feb 2018 10:50:17 +1100 Subject: [PATCH 053/163] setup for auto scroll tests --- ...arshal-types.js => auto-scroller-types.js} | 2 +- ...uto-scroll-marshal.js => auto-scroller.js} | 6 +- src/state/auto-scroll/can-scroll.js | 4 -- .../auto-scroll/create-fluid-scroller.js | 1 - src/window/get-viewport.js | 7 +-- test/unit/state/auto-scroll/auto-scroller.js | 57 +++++++++++++++++++ .../unit/state/auto-scroll/can-scroll.spec.js | 2 +- 7 files changed, 64 insertions(+), 15 deletions(-) rename src/state/auto-scroll/{auto-scroll-marshal-types.js => auto-scroller-types.js} (76%) rename src/state/auto-scroll/{auto-scroll-marshal.js => auto-scroller.js} (90%) create mode 100644 test/unit/state/auto-scroll/auto-scroller.js diff --git a/src/state/auto-scroll/auto-scroll-marshal-types.js b/src/state/auto-scroll/auto-scroller-types.js similarity index 76% rename from src/state/auto-scroll/auto-scroll-marshal-types.js rename to src/state/auto-scroll/auto-scroller-types.js index 7c33b3ae4f..b638b9ad3d 100644 --- a/src/state/auto-scroll/auto-scroll-marshal-types.js +++ b/src/state/auto-scroll/auto-scroller-types.js @@ -1,6 +1,6 @@ // @flow import type { State } from '../../types'; -export type AutoScrollMarshal = {| +export type AutoScroller = {| onStateChange: (previous: State, current: State) => void, |} diff --git a/src/state/auto-scroll/auto-scroll-marshal.js b/src/state/auto-scroll/auto-scroller.js similarity index 90% rename from src/state/auto-scroll/auto-scroll-marshal.js rename to src/state/auto-scroll/auto-scroller.js index abc1b17f0a..7b28ddd857 100644 --- a/src/state/auto-scroll/auto-scroll-marshal.js +++ b/src/state/auto-scroll/auto-scroller.js @@ -3,7 +3,7 @@ import scrollWindow from './scroll-window'; import createFluidScroller, { type FluidScroller } from './create-fluid-scroller'; import createJumpScroller, { type JumpScroller } from './create-jump-scroller'; import { move as moveAction } from '../action-creators'; -import type { AutoScrollMarshal } from './auto-scroll-marshal-types'; +import type { AutoScroller } from './auto-scroller-types'; import type { DroppableId, Position, @@ -18,7 +18,7 @@ type Args = {| export default ({ scrollDroppable, move, -}: Args): AutoScrollMarshal => { +}: Args): AutoScroller => { const fluidScroll: FluidScroller = createFluidScroller({ scrollWindow, scrollDroppable, @@ -58,7 +58,7 @@ export default ({ } }; - const marshal: AutoScrollMarshal = { + const marshal: AutoScroller = { onStateChange, }; diff --git a/src/state/auto-scroll/can-scroll.js b/src/state/auto-scroll/can-scroll.js index d2bc504607..0752e92ea2 100644 --- a/src/state/auto-scroll/can-scroll.js +++ b/src/state/auto-scroll/can-scroll.js @@ -37,10 +37,6 @@ type GetRemainderArgs = {| // cannot be done with a scroll export const getOverlap = (() => { const getRemainder = (target: number, max: number): number => { - if (target === 0) { - return 0; - } - if (target < 0) { return target; } diff --git a/src/state/auto-scroll/create-fluid-scroller.js b/src/state/auto-scroll/create-fluid-scroller.js index b4cb887a3e..d398f9444c 100644 --- a/src/state/auto-scroll/create-fluid-scroller.js +++ b/src/state/auto-scroll/create-fluid-scroller.js @@ -168,7 +168,6 @@ export default ({ const requiredWindowScroll: ?Position = getRequiredScroll(viewport, center); if (requiredWindowScroll && canScrollWindow(requiredWindowScroll)) { - console.log('scheduling window scroll', requiredWindowScroll); scheduleWindowScroll(requiredWindowScroll); return; } diff --git a/src/window/get-viewport.js b/src/window/get-viewport.js index 03fb08cc28..b55f8d4d1f 100644 --- a/src/window/get-viewport.js +++ b/src/window/get-viewport.js @@ -11,14 +11,11 @@ export default (): Area => { const doc: HTMLElement = (document.documentElement : any); - console.log('doc top', document.documentElement.scrollTop); - console.log('window', window.pageYOffset); - - // using these values as they do not consider scrollbars + // Using these values as they do not consider scrollbars const width: number = doc.clientWidth; const height: number = doc.clientHeight; - // computed + // Computed const right: number = left + width; const bottom: number = top + height; diff --git a/test/unit/state/auto-scroll/auto-scroller.js b/test/unit/state/auto-scroll/auto-scroller.js new file mode 100644 index 0000000000..d759a87c0d --- /dev/null +++ b/test/unit/state/auto-scroll/auto-scroller.js @@ -0,0 +1,57 @@ +// @flow + +describe('auto scroller', () => { + describe('fluid scrolling', () => { + describe('on drag', () => { + describe('window scrolling', () => { + it('should not scroll the window if not within the threshold band', () => { + + }); + + it('should not scroll the window if within the threshold area there is no available window scroll', () => { + + }); + + it('should scroll the window if within the threshold area in any direction', () => { + + }); + + it('should not scroll the window if there is no required scroll', () => { + + }); + + describe('window scroll speed', () => { + it('should have a greater scroll speed the closer the user moves to the max speed point', () => { + + }); + + it('should have the max scroll speed once the max speed point is exceeded', () => { + + }); + }); + }); + + describe('droppable scrolling', () => { + + }); + + describe('window scrolling before droppable scrolling', () => { + + }); + }); + + describe('on drag end', () => { + it('should cancel any pending window scroll', () => { + + }); + + it('should cancel any pending droppable scroll', () => { + + }); + }); + }); + + describe('jump scrolling', () => { + + }); +}); diff --git a/test/unit/state/auto-scroll/can-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js index 657e5066ab..0c68bbc784 100644 --- a/test/unit/state/auto-scroll/can-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-scroll.spec.js @@ -467,7 +467,7 @@ describe('can scroll', () => { }); // tested in get remainder - it.only('should return the overlap', () => { + it('should return the overlap', () => { // how far the droppable has already const scroll: Position = { x: 10, y: 20, From 193705ce8a22f6992c52eb8e2188748a44ed14aa Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 5 Feb 2018 14:40:47 +1100 Subject: [PATCH 054/163] more tests --- src/state/auto-scroll/auto-scroller.js | 12 +- .../auto-scroll/create-fluid-scroller.js | 4 +- src/state/auto-scroll/create-jump-scroller.js | 4 +- .../drag-drop-context/drag-drop-context.jsx | 19 ++- .../auto-scroll => window}/scroll-window.js | 3 +- test/setup.js | 11 +- test/unit/state/auto-scroll/auto-scroller.js | 57 ------- .../state/auto-scroll/auto-scroller.spec.js | 155 ++++++++++++++++++ test/utils/set-viewport.js | 44 +++-- test/utils/simple-state-preset.js | 17 +- 10 files changed, 231 insertions(+), 95 deletions(-) rename src/{state/auto-scroll => window}/scroll-window.js (72%) delete mode 100644 test/unit/state/auto-scroll/auto-scroller.js create mode 100644 test/unit/state/auto-scroll/auto-scroller.spec.js diff --git a/src/state/auto-scroll/auto-scroller.js b/src/state/auto-scroll/auto-scroller.js index 7b28ddd857..09795a093d 100644 --- a/src/state/auto-scroll/auto-scroller.js +++ b/src/state/auto-scroll/auto-scroller.js @@ -1,10 +1,9 @@ // @flow -import scrollWindow from './scroll-window'; import createFluidScroller, { type FluidScroller } from './create-fluid-scroller'; import createJumpScroller, { type JumpScroller } from './create-jump-scroller'; -import { move as moveAction } from '../action-creators'; import type { AutoScroller } from './auto-scroller-types'; import type { + DraggableId, DroppableId, Position, State, @@ -12,11 +11,18 @@ import type { type Args = {| scrollDroppable: (id: DroppableId, change: Position) => void, - move: typeof moveAction, + scrollWindow: (change: Position) => void, + move: ( + id: DraggableId, + client: Position, + windowScroll: Position, + shouldAnimate?: boolean + ) => void, |} export default ({ scrollDroppable, + scrollWindow, move, }: Args): AutoScroller => { const fluidScroll: FluidScroller = createFluidScroller({ diff --git a/src/state/auto-scroll/create-fluid-scroller.js b/src/state/auto-scroll/create-fluid-scroller.js index d398f9444c..3cd8fe06f0 100644 --- a/src/state/auto-scroll/create-fluid-scroller.js +++ b/src/state/auto-scroll/create-fluid-scroller.js @@ -38,14 +38,14 @@ const config = { const origin: Position = { x: 0, y: 0 }; -type PixelThresholds = {| +export type PixelThresholds = {| startFrom: number, maxSpeedAt: number, accelerationPlane: number, |} // converts the percentages in the config into actual pixel values -const getPixelThresholds = (container: Area, axis: Axis): PixelThresholds => { +export const getPixelThresholds = (container: Area, axis: Axis): PixelThresholds => { const startFrom: number = container[axis.size] * config.startFrom; const maxSpeedAt: number = container[axis.size] * config.maxSpeedAt; const accelerationPlane: number = startFrom - maxSpeedAt; diff --git a/src/state/auto-scroll/create-jump-scroller.js b/src/state/auto-scroll/create-jump-scroller.js index e1a00b9be2..c733899b64 100644 --- a/src/state/auto-scroll/create-jump-scroller.js +++ b/src/state/auto-scroll/create-jump-scroller.js @@ -3,7 +3,6 @@ import { add } from '../position'; import getWindowScrollPosition from '../../window/get-window-scroll'; import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; import getViewport from '../../window/get-viewport'; -import { move as moveAction } from '../action-creators'; import { canScrollDroppable, canScrollWindow, @@ -11,6 +10,7 @@ import { getDroppableOverlap, } from './can-scroll'; import type { + DraggableId, DroppableId, DragState, DroppableDimension, @@ -24,7 +24,7 @@ import type { type Args = {| scrollDroppable: (id: DroppableId, offset: Position) => void, scrollWindow: (offset: Position) => void, - move: typeof moveAction, + move: (id: DraggableId, client: Position, windowScroll: Position, shouldAnimate?: boolean) => void, |} export type JumpScroller = (state: State) => void; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index e2ace94f04..74d8f1d3ae 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -6,8 +6,9 @@ import fireHooks from '../../state/fire-hooks'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; import createStyleMarshal from '../style-marshal/style-marshal'; import canStartDrag from '../../state/can-start-drag'; -import createAutoScroll from '../../state/auto-scroll/auto-scroll-marshal'; -import type { AutoScrollMarshal } from '../../state/auto-scroll/auto-scroll-marshal-types'; +import scrollWindow from '../../window/scroll-window'; +import createAutoScroller from '../../state/auto-scroll/auto-scroller'; +import type { AutoScroller } from '../../state/auto-scroll/auto-scroller-types'; import type { StyleMarshal } from '../style-marshal/style-marshal-types'; import type { DimensionMarshal, @@ -52,7 +53,7 @@ export default class DragDropContext extends React.Component { store: Store dimensionMarshal: DimensionMarshal styleMarshal: StyleMarshal - scrollMarshal: AutoScrollMarshal + autoScroller: AutoScroller unsubscribe: Function // Need to declare childContextTypes without flow @@ -112,9 +113,15 @@ export default class DragDropContext extends React.Component { }, }; this.dimensionMarshal = createDimensionMarshal(callbacks); - this.scrollMarshal = createAutoScroll({ + this.autoScroller = createAutoScroller({ + scrollWindow, scrollDroppable: this.dimensionMarshal.scrollDroppable, - move: (id: DraggableId, client: Position, windowScroll: Position, shouldAnimate: boolean) => { + move: ( + id: DraggableId, + client: Position, + windowScroll: Position, + shouldAnimate?: boolean + ): void => { this.store.dispatch(move(id, client, windowScroll, shouldAnimate)); }, }); @@ -139,7 +146,7 @@ export default class DragDropContext extends React.Component { } onStateChange(previous: State, current: State) { - this.scrollMarshal.onStateChange(previous, current); + this.autoScroller.onStateChange(previous, current); } onPhaseChange(previous: State, current: State) { diff --git a/src/state/auto-scroll/scroll-window.js b/src/window/scroll-window.js similarity index 72% rename from src/state/auto-scroll/scroll-window.js rename to src/window/scroll-window.js index fd07f6e71a..8d33773e58 100644 --- a/src/state/auto-scroll/scroll-window.js +++ b/src/window/scroll-window.js @@ -1,11 +1,10 @@ // @flow import type { Position, -} from '../../types'; +} from '../types'; // Not guarenteed to scroll by the entire amount export default (change: Position): void => { - console.log('scrolling window', change); window.scrollBy(change.x, change.y); }; diff --git a/test/setup.js b/test/setup.js index b44902885c..fe17a64af6 100644 --- a/test/setup.js +++ b/test/setup.js @@ -9,12 +9,11 @@ if (typeof window !== 'undefined') { // overriding these properties in jsdom to allow them to be controlled - Object.defineProperty(document.documentElement, 'scrollHeight', { - writable: true, - }); - - Object.defineProperty(document.documentElement, 'scrollWidth', { - writable: true, + Object.defineProperties(document.documentElement, { + clientWidth: { writable: true, value: document.documentElement.clientWidth }, + clientHeight: { writable: true, value: document.documentElement.clientHeight }, + scrollWidth: { writable: true, value: document.documentElement.scrollWidth }, + scrollHeight: { writable: true, value: document.documentElement.scrollHeight }, }); } diff --git a/test/unit/state/auto-scroll/auto-scroller.js b/test/unit/state/auto-scroll/auto-scroller.js deleted file mode 100644 index d759a87c0d..0000000000 --- a/test/unit/state/auto-scroll/auto-scroller.js +++ /dev/null @@ -1,57 +0,0 @@ -// @flow - -describe('auto scroller', () => { - describe('fluid scrolling', () => { - describe('on drag', () => { - describe('window scrolling', () => { - it('should not scroll the window if not within the threshold band', () => { - - }); - - it('should not scroll the window if within the threshold area there is no available window scroll', () => { - - }); - - it('should scroll the window if within the threshold area in any direction', () => { - - }); - - it('should not scroll the window if there is no required scroll', () => { - - }); - - describe('window scroll speed', () => { - it('should have a greater scroll speed the closer the user moves to the max speed point', () => { - - }); - - it('should have the max scroll speed once the max speed point is exceeded', () => { - - }); - }); - }); - - describe('droppable scrolling', () => { - - }); - - describe('window scrolling before droppable scrolling', () => { - - }); - }); - - describe('on drag end', () => { - it('should cancel any pending window scroll', () => { - - }); - - it('should cancel any pending droppable scroll', () => { - - }); - }); - }); - - describe('jump scrolling', () => { - - }); -}); diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js new file mode 100644 index 0000000000..eb0cdf2ad4 --- /dev/null +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -0,0 +1,155 @@ +// @flow +import type { + Area, + Axis, + Position, + State, +} from '../../../../src/types'; +import type { AutoScroller } from '../../../../src/state/auto-scroll/auto-scroller-types'; +import type { PixelThresholds } from '../../../../src/state/auto-scroll/create-fluid-scroller'; +import { getPixelThresholds } from '../../../../src/state/auto-scroll/create-fluid-scroller'; +import setViewport, { resetViewport } from '../../../utils/set-viewport'; +import { patch } from '../../../../src/state/position'; +import getArea from '../../../../src/state/get-area'; +import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; +import { vertical, horizontal } from '../../../../src/state/axis'; +import createAutoScroller from '../../../../src/state/auto-scroll/auto-scroller'; +import * as state from '../../../utils/simple-state-preset'; +import { getPreset } from '../../../utils/dimension'; + +describe('auto scroller', () => { + let autoScroller: AutoScroller; + let mocks; + + beforeEach(() => { + mocks = { + scrollWindow: jest.fn(), + scrollDroppable: jest.fn(), + move: jest.fn(), + }; + autoScroller = createAutoScroller(mocks); + }); + afterEach(() => { + // resetViewport(); + resetWindowScrollSize(); + requestAnimationFrame.reset(); + }); + + describe('fluid scrolling', () => { + describe('on drag', () => { + const viewport: Area = getArea({ + top: 0, + left: 0, + right: 800, + bottom: 1000, + }); + + beforeEach(() => { + setViewport(viewport); + setWindowScrollSize({ + scrollHeight: 2000, + scrollWidth: 1600, + }); + }); + + describe('window scrolling', () => { + [vertical].forEach((axis: Axis) => { + describe(`on the ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); + const dragTo = (selection: Position): State => + state.dragging(preset.inHome1.descriptor.id, selection); + + describe('moving forward to end of window', () => { + it('should not scroll if not past the start threshold', () => { + const target: Position = patch( + axis.line, + // to the boundary is not enough to start + (viewport[axis.size] - thresholds.startFrom), + viewport.center[axis.crossLine], + ); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + + it('should scroll if to the start threshold', () => { + const target: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.startFrom) + 1, + viewport.center[axis.crossLine], + ); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + }); + + it('should throttle multiple scrolls into a single animation frame', () => { + + }); + + it('should get faster the closer to the max speed point', () => { + + }); + + it('should have the top speed at the max speed point', () => { + + }); + + it('should have the top speed when moving beyond the max speed point', () => { + + }); + }); + }); + }); + + it('should not scroll the window if there is no required scroll', () => { + + }); + + describe('window scroll speed', () => { + it('should have a greater scroll speed the closer the user moves to the max speed point', () => { + + }); + + it('should have the max scroll speed once the max speed point is exceeded', () => { + + }); + }); + + describe('subject is too big for auto scrolling', () => { + + }); + }); + + describe('droppable scrolling', () => { + + }); + + describe('window scrolling before droppable scrolling', () => { + + }); + }); + + describe('on drag end', () => { + it('should cancel any pending window scroll', () => { + + }); + + it('should cancel any pending droppable scroll', () => { + + }); + }); + }); + + describe('jump scrolling', () => { + + }); +}); diff --git a/test/utils/set-viewport.js b/test/utils/set-viewport.js index c299989a6b..880dfc9183 100644 --- a/test/utils/set-viewport.js +++ b/test/utils/set-viewport.js @@ -2,19 +2,43 @@ import type { Area } from '../../src/types'; import getArea from '../../src/state/get-area'; +const getDoc = (): HTMLElement => { + const el: ?HTMLElement = document.documentElement; + + if (!el) { + throw new Error('Unable to get document.documentElement'); + } + + return el; +}; + const setViewport = (custom: Area) => { - window.pageYOffset = custom.top; - window.pageXOffset = custom.left; - window.innerWidth = custom.width; - window.innerHeight = custom.height; + if (custom.top !== 0 || custom.left !== 0) { + throw new Error('not setting window scroll with setViewport. Use set-window-scroll'); + } + + if (window.pageXOffset !== 0 || window.pageYOffset !== 0) { + throw new Error('Setting viewport on scrolled window'); + } + + window.pageYOffset = 0; + window.pageXOffset = 0; + + const doc: HTMLElement = getDoc(); + doc.clientWidth = custom.width; + doc.clientHeight = custom.height; }; -export const getCurrent = (): Area => getArea({ - top: window.pageYOffset, - left: window.pageXOffset, - width: window.innerWidth, - height: window.innerHeight, -}); +export const getCurrent = (): Area => { + const doc: HTMLElement = getDoc(); + + return getArea({ + top: window.pageYOffset, + left: window.pageXOffset, + width: doc.clientWidth, + height: doc.clientHeight, + }); +}; const original: Area = getCurrent(); diff --git a/test/utils/simple-state-preset.js b/test/utils/simple-state-preset.js index 49f338ce70..8da8027186 100644 --- a/test/utils/simple-state-preset.js +++ b/test/utils/simple-state-preset.js @@ -65,25 +65,27 @@ export const requesting = (request?: DraggableId = preset.inHome1.descriptor.id) const origin: Position = { x: 0, y: 0 }; export const dragging = ( - id?: DraggableId = preset.inHome1.descriptor.id + id?: DraggableId = preset.inHome1.descriptor.id, + selection?: Position, ): State => { // will populate the dimension state with the initial dimensions const draggable: DraggableDimension = preset.draggables[id]; - const client: Position = draggable.client.withMargin.center; + // either use the provided selection or use the draggable's center + const clientSelection: Position = selection || draggable.client.withMargin.center; const initialPosition: InitialDragPositions = { - selection: client, - center: client, + selection: clientSelection, + center: clientSelection, }; const clientPositions: CurrentDragPositions = { - selection: client, - center: client, + selection: clientSelection, + center: clientSelection, offset: origin, }; const drag: DragState = { initial: { descriptor: draggable.descriptor, - isScrollAllowed: true, + autoScrollMode: 'FLUID', client: initialPosition, page: initialPosition, windowScroll: origin, @@ -95,6 +97,7 @@ export const dragging = ( shouldAnimate: false, }, impact: noImpact, + scrollJumpRequest: null, }; const result: State = { From 2da64cca6be66b755bf511249463717edd0875e1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 5 Feb 2018 20:43:22 +1100 Subject: [PATCH 055/163] adding tests --- .../auto-scroll/create-fluid-scroller.js | 9 +- .../state/auto-scroll/auto-scroller.spec.js | 143 ++++++++++++++++-- test/unit/state/position.spec.js | 13 ++ 3 files changed, 147 insertions(+), 18 deletions(-) diff --git a/src/state/auto-scroll/create-fluid-scroller.js b/src/state/auto-scroll/create-fluid-scroller.js index 3cd8fe06f0..e039799de5 100644 --- a/src/state/auto-scroll/create-fluid-scroller.js +++ b/src/state/auto-scroll/create-fluid-scroller.js @@ -1,7 +1,7 @@ // @flow import rafSchd from 'raf-schd'; import getViewport from '../../window/get-viewport'; -import { isEqual } from '../position'; +import { apply, isEqual } from '../position'; import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; import getScrollableDroppableOver from './get-scrollable-droppable-over'; import { horizontal, vertical } from '../axis'; @@ -23,7 +23,7 @@ import type { } from '../../types'; // Values used to control how the fluid auto scroll feels -const config = { +export const config = { // percentage distance from edge of container: startFrom: 0.25, maxSpeedAt: 0.05, @@ -38,6 +38,9 @@ const config = { const origin: Position = { x: 0, y: 0 }; +// will replace -0 and replace with +0 +const clean = apply((value: number) => (value === 0 ? 0 : value)); + export type PixelThresholds = {| startFrom: number, maxSpeedAt: number, @@ -125,7 +128,7 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { return -1 * getSpeed(distance.left, thresholds); })(); - const required: Position = { x, y }; + const required: Position = clean({ x, y }); return isEqual(required, origin) ? null : required; }; diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index eb0cdf2ad4..70bb9f4147 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -4,10 +4,12 @@ import type { Axis, Position, State, + DraggableDimension, + Spacing, } from '../../../../src/types'; import type { AutoScroller } from '../../../../src/state/auto-scroll/auto-scroller-types'; import type { PixelThresholds } from '../../../../src/state/auto-scroll/create-fluid-scroller'; -import { getPixelThresholds } from '../../../../src/state/auto-scroll/create-fluid-scroller'; +import { getPixelThresholds, config } from '../../../../src/state/auto-scroll/create-fluid-scroller'; import setViewport, { resetViewport } from '../../../utils/set-viewport'; import { patch } from '../../../../src/state/position'; import getArea from '../../../../src/state/get-area'; @@ -16,6 +18,8 @@ import { vertical, horizontal } from '../../../../src/state/axis'; import createAutoScroller from '../../../../src/state/auto-scroll/auto-scroller'; import * as state from '../../../utils/simple-state-preset'; import { getPreset } from '../../../utils/dimension'; +import { expandByPosition } from '../../../../src/state/spacing'; +import { getDraggableDimension } from '../../../../src/state/dimension'; describe('auto scroller', () => { let autoScroller: AutoScroller; @@ -53,7 +57,7 @@ describe('auto scroller', () => { }); describe('window scrolling', () => { - [vertical].forEach((axis: Axis) => { + [vertical, horizontal].forEach((axis: Axis) => { describe(`on the ${axis.direction} axis`, () => { const preset = getPreset(axis); const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); @@ -92,39 +96,148 @@ describe('auto scroller', () => { }); it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.startFrom) + 1, + viewport.center[axis.crossLine], + ); + const target2: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.startFrom) + 2, + viewport.center[axis.crossLine], + ); + + autoScroller.onStateChange(state.idle, dragTo(target1)); + autoScroller.onStateChange(state.idle, dragTo(target2)); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + // not testing value called as we are not exposing getRequired scroll }); it('should get faster the closer to the max speed point', () => { + const target1: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.startFrom) + 1, + viewport.center[axis.crossLine], + ); + const target2: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.startFrom) + 2, + viewport.center[axis.crossLine], + ); + autoScroller.onStateChange(state.idle, dragTo(target1)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); + + autoScroller.onStateChange(state.idle, dragTo(target2)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); + + expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); + + // validation + expect(scroll1[axis.crossLine]).toBe(0); + expect(scroll2[axis.crossLine]).toBe(0); }); it('should have the top speed at the max speed point', () => { + const target: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossLine], + ); + const expected: Position = patch(axis.line, config.maxScrollSpeed); + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); + + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); }); it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = patch( + axis.line, + // gone beyond the max scroll at point + (viewport[axis.size] - thresholds.maxSpeedAt) + 1, + viewport.center[axis.crossLine], + ); + const expected: Position = patch(axis.line, config.maxScrollSpeed); - }); - }); - }); - }); - - it('should not scroll the window if there is no required scroll', () => { + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); - }); + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); - describe('window scroll speed', () => { - it('should have a greater scroll speed the closer the user moves to the max speed point', () => { + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + const selection: Position = patch( + axis.line, + // gone beyond the max scroll at point + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossLine], + ); + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + return { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, + }, + }, + dimension: { + ...base.dimension, + draggable: { + ...base.dimension.draggable, + [tooBig.descriptor.id]: tooBig, + }, + }, + }; + })(); + + autoScroller.onStateChange(state.idle, custom); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + }); - it('should have the max scroll speed once the max speed point is exceeded', () => { + describe('moving backwards towards the start of window', () => { + }); }); }); - describe('subject is too big for auto scrolling', () => { + describe('it should not scroll if too big', () => { }); }); @@ -134,7 +247,7 @@ describe('auto scroller', () => { }); describe('window scrolling before droppable scrolling', () => { - + // TODO: if window scrolling - do not droppable scroll }); }); diff --git a/test/unit/state/position.spec.js b/test/unit/state/position.spec.js index 6d97b55aca..cd89a8a282 100644 --- a/test/unit/state/position.spec.js +++ b/test/unit/state/position.spec.js @@ -1,6 +1,7 @@ // @flow import { add, + apply, subtract, isEqual, negate, @@ -50,6 +51,10 @@ describe('position', () => { it('should return false when two objects have different values', () => { expect(isEqual(point1, point2)).toBe(false); }); + + it('should return true when -origin is compared with +origin', () => { + expect(isEqual({ x: -0, y: -0 }, { x: 0, y: 0 })).toBe(true); + }); }); describe('negate', () => { @@ -125,4 +130,12 @@ describe('position', () => { expect(closest(origin, [option1, option2])).toEqual(distance(origin, option1)); }); }); + + describe('apply', () => { + it('should apply the function to both values', () => { + const add1 = apply((value: number) => value + 1); + + expect(add1({ x: 1, y: 2 })).toEqual({ x: 2, y: 3 }); + }); + }); }); From 53584bfcc72262e3464462a063d3209ee1cc2b8e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 6 Feb 2018 09:24:54 +1100 Subject: [PATCH 056/163] more tests --- .../state/auto-scroll/auto-scroller.spec.js | 186 +++++++++++++++++- 1 file changed, 180 insertions(+), 6 deletions(-) diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index 70bb9f4147..63aae5e153 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -14,6 +14,7 @@ import setViewport, { resetViewport } from '../../../utils/set-viewport'; import { patch } from '../../../../src/state/position'; import getArea from '../../../../src/state/get-area'; import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; +import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; import { vertical, horizontal } from '../../../../src/state/axis'; import createAutoScroller from '../../../../src/state/auto-scroll/auto-scroller'; import * as state from '../../../utils/simple-state-preset'; @@ -34,8 +35,9 @@ describe('auto scroller', () => { autoScroller = createAutoScroller(mocks); }); afterEach(() => { - // resetViewport(); + resetWindowScroll(); resetWindowScrollSize(); + resetViewport(); requestAnimationFrame.reset(); }); @@ -79,7 +81,7 @@ describe('auto scroller', () => { expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); - it('should scroll if to the start threshold', () => { + it('should scroll if moving beyond the start threshold', () => { const target: Position = patch( axis.line, (viewport[axis.size] - thresholds.startFrom) + 1, @@ -93,6 +95,9 @@ describe('auto scroller', () => { // only called after a frame requestAnimationFrame.step(); expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving forwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.line]).toBeGreaterThan(0); }); it('should throttle multiple scrolls into a single animation frame', () => { @@ -232,13 +237,182 @@ describe('auto scroller', () => { }); describe('moving backwards towards the start of window', () => { + const windowScroll: Position = patch(axis.line, 10); + beforeEach(() => { + setWindowScroll(windowScroll); + }); - }); - }); - }); + it('should not scroll if not past the start threshold', () => { + const target: Position = patch( + axis.line, + // at the boundary is not enough to start + windowScroll[axis.line] + (thresholds.startFrom), + viewport.center[axis.crossLine], + ); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - describe('it should not scroll if too big', () => { + it('should scroll if moving beyond the start threshold', () => { + const target: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.startFrom) - 1, + viewport.center[axis.crossLine], + ); + autoScroller.onStateChange(state.idle, dragTo(target)); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving backwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.line]).toBeLessThan(0); + }); + + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.startFrom) - 1, + viewport.center[axis.crossLine], + ); + const target2: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.startFrom) - 2, + viewport.center[axis.crossLine], + ); + + autoScroller.onStateChange(state.idle, dragTo(target1)); + autoScroller.onStateChange(state.idle, dragTo(target2)); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + + // not testing value called as we are not exposing getRequired scroll + }); + + it('should get faster the closer to the max speed point', () => { + const target1: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.startFrom) - 1, + viewport.center[axis.crossLine], + ); + const target2: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.startFrom) - 2, + viewport.center[axis.crossLine], + ); + + autoScroller.onStateChange(state.idle, dragTo(target1)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); + + autoScroller.onStateChange(state.idle, dragTo(target2)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); + + // moving backwards so a smaller value is bigger + expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); + // or put another way: + expect(Math.abs(scroll1[axis.line])).toBeLessThan(Math.abs(scroll2[axis.line])); + + // validation + expect(scroll1[axis.crossLine]).toBe(0); + expect(scroll2[axis.crossLine]).toBe(0); + }); + + it('should have the top speed at the max speed point', () => { + const target: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.maxSpeedAt), + viewport.center[axis.crossLine], + ); + const expected: Position = patch(axis.line, -config.maxScrollSpeed); + + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); + + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); + + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = patch( + axis.line, + // gone beyond the max scroll at point + (windowScroll[axis.line] + thresholds.maxSpeedAt) - 1, + viewport.center[axis.crossLine], + ); + const expected: Position = patch(axis.line, -config.maxScrollSpeed); + + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); + + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); + + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + const selection: Position = patch( + axis.line, + windowScroll[axis.line] + thresholds.maxSpeedAt, + viewport.center[axis.crossLine], + ); + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + return { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, + }, + }, + dimension: { + ...base.dimension, + draggable: { + ...base.dimension.draggable, + [tooBig.descriptor.id]: tooBig, + }, + }, + }; + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + }); + }); }); }); From e5d9b98b17c05eb0fb2dcd4f066a9e4ee120b99a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 6 Feb 2018 10:26:08 +1100 Subject: [PATCH 057/163] tests for cross axis scrolling --- .../state/auto-scroll/auto-scroller.spec.js | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index 63aae5e153..61c9650cec 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -234,6 +234,23 @@ describe('auto scroller', () => { requestAnimationFrame.flush(); expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); + + it('should not scroll if the window cannot scroll', () => { + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, + }); + const target: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.startFrom) + 1, + viewport.center[axis.crossLine], + ); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + requestAnimationFrame.step(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); }); describe('moving backwards towards the start of window', () => { @@ -411,6 +428,109 @@ describe('auto scroller', () => { requestAnimationFrame.flush(); expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); + + it('should not scroll if the window cannot scroll', () => { + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, + }); + const target: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.startFrom) - 1, + viewport.center[axis.crossLine], + ); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + requestAnimationFrame.step(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + }); + + // just some light tests to ensure that cross axis moving also works + describe('moving forward on the cross axis', () => { + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + viewport, + axis === vertical ? horizontal : vertical, + ); + + it('should not scroll if not past the start threshold', () => { + const target: Position = patch( + axis.line, + viewport.center[axis.line], + // to the boundary is not enough to start + (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom), + ); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + + it('should scroll if moving beyond the start threshold', () => { + const target: Position = patch( + axis.line, + viewport.center[axis.line], + (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom) + 1, + ); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving forwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.crossLine]).toBeGreaterThan(0); + }); + }); + + describe('moving backward on the cross axis', () => { + const windowScroll: Position = patch(axis.crossLine, 10); + beforeEach(() => { + setWindowScroll(windowScroll); + }); + + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + viewport, + axis === vertical ? horizontal : vertical, + ); + + it('should not scroll if not past the start threshold', () => { + const target: Position = patch( + axis.line, + viewport.center[axis.line], + // to the boundary is not enough to start + windowScroll[axis.crossLine] + (crossAxisThresholds.startFrom) + ); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + + it('should scroll if moving beyond the start threshold', () => { + const target: Position = patch( + axis.line, + viewport.center[axis.line], + (windowScroll[axis.crossLine] + crossAxisThresholds.startFrom) - 1 + ); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving backwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.crossLine]).toBeLessThan(0); + }); }); }); }); From 597cfd4b730e0655461da85239ce4e961b8e0dfd Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 6 Feb 2018 10:46:05 +1100 Subject: [PATCH 058/163] about to try something cray --- .../state/auto-scroll/auto-scroller.spec.js | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index 61c9650cec..10de294d88 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -58,9 +58,16 @@ describe('auto scroller', () => { }); }); - describe('window scrolling', () => { - [vertical, horizontal].forEach((axis: Axis) => { - describe(`on the ${axis.direction} axis`, () => { + [vertical, horizontal].forEach((axis: Axis) => { + describe(`on the ${axis.direction} axis`, () => { + type Case = {| + name: string, + scroll: (change: Position) => void, + didScroll: () => boolean, + area: Area, + |} + + describe('window scrolling', () => { const preset = getPreset(axis); const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); const dragTo = (selection: Position): State => @@ -533,25 +540,25 @@ describe('auto scroller', () => { }); }); }); - }); - }); - describe('droppable scrolling', () => { + describe('droppable scrolling', () => { - }); + }); - describe('window scrolling before droppable scrolling', () => { - // TODO: if window scrolling - do not droppable scroll - }); - }); + describe('window scrolling before droppable scrolling', () => { + // TODO: if window scrolling - do not droppable scroll + }); + }); - describe('on drag end', () => { - it('should cancel any pending window scroll', () => { + describe('on drag end', () => { + it('should cancel any pending window scroll', () => { - }); + }); - it('should cancel any pending droppable scroll', () => { + it('should cancel any pending droppable scroll', () => { + }); + }); }); }); }); From c2c088250e89972c5dc30f9fc50b40092da76adb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 6 Feb 2018 11:37:01 +1100 Subject: [PATCH 059/163] simplifying some tests --- .../state/auto-scroll/auto-scroller.spec.js | 341 +++++++++--------- .../unit/state/auto-scroll/can-scroll.spec.js | 1 + 2 files changed, 177 insertions(+), 165 deletions(-) diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index 10de294d88..e0e8980a56 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -5,13 +5,13 @@ import type { Position, State, DraggableDimension, - Spacing, + DroppableDimension, } from '../../../../src/types'; import type { AutoScroller } from '../../../../src/state/auto-scroll/auto-scroller-types'; import type { PixelThresholds } from '../../../../src/state/auto-scroll/create-fluid-scroller'; import { getPixelThresholds, config } from '../../../../src/state/auto-scroll/create-fluid-scroller'; import setViewport, { resetViewport } from '../../../utils/set-viewport'; -import { patch } from '../../../../src/state/position'; +import { add, patch, subtract } from '../../../../src/state/position'; import getArea from '../../../../src/state/get-area'; import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; @@ -20,7 +20,29 @@ import createAutoScroller from '../../../../src/state/auto-scroll/auto-scroller' import * as state from '../../../utils/simple-state-preset'; import { getPreset } from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; -import { getDraggableDimension } from '../../../../src/state/dimension'; +import { getDraggableDimension, getDroppableDimension } from '../../../../src/state/dimension'; + +const addDroppable = (base: State, droppable: DroppableDimension): State => ({ + ...base, + dimension: { + ...base.dimension, + droppable: { + ...base.dimension.droppable, + [droppable.descriptor.id]: droppable, + }, + }, +}); + +const addDraggable = (base: State, draggable: DraggableDimension): State => ({ + ...base, + dimension: { + ...base.dimension, + draggable: { + ...base.dimension.draggable, + [draggable.descriptor.id]: draggable, + }, + }, +}); describe('auto scroller', () => { let autoScroller: AutoScroller; @@ -60,40 +82,34 @@ describe('auto scroller', () => { [vertical, horizontal].forEach((axis: Axis) => { describe(`on the ${axis.direction} axis`, () => { - type Case = {| - name: string, - scroll: (change: Position) => void, - didScroll: () => boolean, - area: Area, - |} + const preset = getPreset(axis); + const dragTo = (selection: Position): State => + state.dragging(preset.inHome1.descriptor.id, selection); describe('window scrolling', () => { - const preset = getPreset(axis); const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); - const dragTo = (selection: Position): State => - state.dragging(preset.inHome1.descriptor.id, selection); - describe('moving forward to end of window', () => { - it('should not scroll if not past the start threshold', () => { - const target: Position = patch( - axis.line, - // to the boundary is not enough to start - (viewport[axis.size] - thresholds.startFrom), - viewport.center[axis.crossLine], - ); + const onStartBoundary: Position = patch( + axis.line, + // to the boundary is not enough to start + (viewport[axis.size] - thresholds.startFrom), + viewport.center[axis.crossLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossLine], + ); - autoScroller.onStateChange(state.idle, dragTo(target)); + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); requestAnimationFrame.flush(); expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); it('should scroll if moving beyond the start threshold', () => { - const target: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.startFrom) + 1, - viewport.center[axis.crossLine], - ); + const target: Position = add(onStartBoundary, patch(axis.line, 1)); autoScroller.onStateChange(state.idle, dragTo(target)); @@ -108,16 +124,8 @@ describe('auto scroller', () => { }); it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.startFrom) + 1, - viewport.center[axis.crossLine], - ); - const target2: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.startFrom) + 2, - viewport.center[axis.crossLine], - ); + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); autoScroller.onStateChange(state.idle, dragTo(target1)); autoScroller.onStateChange(state.idle, dragTo(target2)); @@ -136,16 +144,8 @@ describe('auto scroller', () => { }); it('should get faster the closer to the max speed point', () => { - const target1: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.startFrom) + 1, - viewport.center[axis.crossLine], - ); - const target2: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.startFrom) + 2, - viewport.center[axis.crossLine], - ); + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); autoScroller.onStateChange(state.idle, dragTo(target1)); requestAnimationFrame.step(); @@ -165,26 +165,16 @@ describe('auto scroller', () => { }); it('should have the top speed at the max speed point', () => { - const target: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.maxSpeedAt), - viewport.center[axis.crossLine], - ); const expected: Position = patch(axis.line, config.maxScrollSpeed); - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); requestAnimationFrame.step(); expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); }); it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = patch( - axis.line, - // gone beyond the max scroll at point - (viewport[axis.size] - thresholds.maxSpeedAt) + 1, - viewport.center[axis.crossLine], - ); + const target: Position = add(onMaxBoundary, patch(axis.line, 1)); const expected: Position = patch(axis.line, config.maxScrollSpeed); autoScroller.onStateChange(state.idle, dragTo(target)); @@ -204,19 +194,14 @@ describe('auto scroller', () => { }, client: expanded, }); - const selection: Position = patch( - axis.line, - // gone beyond the max scroll at point - (viewport[axis.size] - thresholds.maxSpeedAt), - viewport.center[axis.crossLine], - ); + const selection: Position = onMaxBoundary; const custom: State = (() => { const base: State = state.dragging( preset.inHome1.descriptor.id, selection, ); - return { + const updated: State = { ...base, drag: { ...base.drag, @@ -226,14 +211,9 @@ describe('auto scroller', () => { descriptor: tooBig.descriptor, }, }, - dimension: { - ...base.dimension, - draggable: { - ...base.dimension.draggable, - [tooBig.descriptor.id]: tooBig, - }, - }, }; + + return addDraggable(updated, tooBig); })(); autoScroller.onStateChange(state.idle, custom); @@ -247,11 +227,7 @@ describe('auto scroller', () => { scrollHeight: viewport.height, scrollWidth: viewport.width, }); - const target: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.startFrom) + 1, - viewport.center[axis.crossLine], - ); + const target: Position = onMaxBoundary; autoScroller.onStateChange(state.idle, dragTo(target)); @@ -262,30 +238,32 @@ describe('auto scroller', () => { describe('moving backwards towards the start of window', () => { const windowScroll: Position = patch(axis.line, 10); + beforeEach(() => { setWindowScroll(windowScroll); }); - it('should not scroll if not past the start threshold', () => { - const target: Position = patch( - axis.line, - // at the boundary is not enough to start - windowScroll[axis.line] + (thresholds.startFrom), - viewport.center[axis.crossLine], - ); + const onStartBoundary: Position = patch( + axis.line, + // at the boundary is not enough to start + windowScroll[axis.line] + (thresholds.startFrom), + viewport.center[axis.crossLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.maxSpeedAt), + viewport.center[axis.crossLine], + ); - autoScroller.onStateChange(state.idle, dragTo(target)); + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); requestAnimationFrame.flush(); expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); it('should scroll if moving beyond the start threshold', () => { - const target: Position = patch( - axis.line, - (windowScroll[axis.line] + thresholds.startFrom) - 1, - viewport.center[axis.crossLine], - ); + const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); autoScroller.onStateChange(state.idle, dragTo(target)); @@ -300,16 +278,8 @@ describe('auto scroller', () => { }); it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = patch( - axis.line, - (windowScroll[axis.line] + thresholds.startFrom) - 1, - viewport.center[axis.crossLine], - ); - const target2: Position = patch( - axis.line, - (windowScroll[axis.line] + thresholds.startFrom) - 2, - viewport.center[axis.crossLine], - ); + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); autoScroller.onStateChange(state.idle, dragTo(target1)); autoScroller.onStateChange(state.idle, dragTo(target2)); @@ -328,16 +298,8 @@ describe('auto scroller', () => { }); it('should get faster the closer to the max speed point', () => { - const target1: Position = patch( - axis.line, - (windowScroll[axis.line] + thresholds.startFrom) - 1, - viewport.center[axis.crossLine], - ); - const target2: Position = patch( - axis.line, - (windowScroll[axis.line] + thresholds.startFrom) - 2, - viewport.center[axis.crossLine], - ); + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); autoScroller.onStateChange(state.idle, dragTo(target1)); requestAnimationFrame.step(); @@ -360,11 +322,7 @@ describe('auto scroller', () => { }); it('should have the top speed at the max speed point', () => { - const target: Position = patch( - axis.line, - (windowScroll[axis.line] + thresholds.maxSpeedAt), - viewport.center[axis.crossLine], - ); + const target: Position = onMaxBoundary; const expected: Position = patch(axis.line, -config.maxScrollSpeed); autoScroller.onStateChange(state.idle, dragTo(target)); @@ -374,12 +332,7 @@ describe('auto scroller', () => { }); it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = patch( - axis.line, - // gone beyond the max scroll at point - (windowScroll[axis.line] + thresholds.maxSpeedAt) - 1, - viewport.center[axis.crossLine], - ); + const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); const expected: Position = patch(axis.line, -config.maxScrollSpeed); autoScroller.onStateChange(state.idle, dragTo(target)); @@ -399,18 +352,14 @@ describe('auto scroller', () => { }, client: expanded, }); - const selection: Position = patch( - axis.line, - windowScroll[axis.line] + thresholds.maxSpeedAt, - viewport.center[axis.crossLine], - ); + const selection: Position = onMaxBoundary; const custom: State = (() => { const base: State = state.dragging( preset.inHome1.descriptor.id, selection, ); - return { + const updated: State = { ...base, drag: { ...base.drag, @@ -420,14 +369,9 @@ describe('auto scroller', () => { descriptor: tooBig.descriptor, }, }, - dimension: { - ...base.dimension, - draggable: { - ...base.dimension.draggable, - [tooBig.descriptor.id]: tooBig, - }, - }, }; + + return addDraggable(updated, tooBig); })(); autoScroller.onStateChange(state.idle, custom); @@ -441,11 +385,7 @@ describe('auto scroller', () => { scrollHeight: viewport.height, scrollWidth: viewport.width, }); - const target: Position = patch( - axis.line, - (windowScroll[axis.line] + thresholds.startFrom) - 1, - viewport.center[axis.crossLine], - ); + const target: Position = onMaxBoundary; autoScroller.onStateChange(state.idle, dragTo(target)); @@ -461,26 +401,22 @@ describe('auto scroller', () => { axis === vertical ? horizontal : vertical, ); - it('should not scroll if not past the start threshold', () => { - const target: Position = patch( - axis.line, - viewport.center[axis.line], - // to the boundary is not enough to start - (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom), - ); + const onStartBoundary: Position = patch( + axis.line, + viewport.center[axis.line], + // to the boundary is not enough to start + (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom), + ); - autoScroller.onStateChange(state.idle, dragTo(target)); + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); requestAnimationFrame.flush(); expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); it('should scroll if moving beyond the start threshold', () => { - const target: Position = patch( - axis.line, - viewport.center[axis.line], - (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom) + 1, - ); + const target: Position = add(onStartBoundary, patch(axis.crossLine, 1)); autoScroller.onStateChange(state.idle, dragTo(target)); @@ -506,26 +442,22 @@ describe('auto scroller', () => { axis === vertical ? horizontal : vertical, ); - it('should not scroll if not past the start threshold', () => { - const target: Position = patch( - axis.line, - viewport.center[axis.line], - // to the boundary is not enough to start - windowScroll[axis.crossLine] + (crossAxisThresholds.startFrom) - ); + const onStartBoundary: Position = patch( + axis.line, + viewport.center[axis.line], + // to the boundary is not enough to start + windowScroll[axis.crossLine] + (crossAxisThresholds.startFrom) + ); - autoScroller.onStateChange(state.idle, dragTo(target)); + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); requestAnimationFrame.flush(); expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); it('should scroll if moving beyond the start threshold', () => { - const target: Position = patch( - axis.line, - viewport.center[axis.line], - (windowScroll[axis.crossLine] + crossAxisThresholds.startFrom) - 1 - ); + const target: Position = subtract(onStartBoundary, patch(axis.crossLine, 1)); autoScroller.onStateChange(state.idle, dragTo(target)); @@ -542,7 +474,86 @@ describe('auto scroller', () => { }); describe('droppable scrolling', () => { + const scrollableScrollSize = { + scrollWidth: 800, + scrollHeight: 800, + }; + const frame: Area = getArea({ + top: 0, + left: 0, + right: 600, + bottom: 600, + }); + const scrollable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'drop-1', + type: 'TYPE', + }, + client: getArea({ + top: 0, + left: 0, + // bigger than the frame + right: scrollableScrollSize.scrollWidth, + bottom: scrollableScrollSize.scrollHeight, + }), + closest: { + frameClient: frame, + scrollWidth: scrollableScrollSize.scrollWidth, + scrollHeight: scrollableScrollSize.scrollHeight, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + const thresholds: PixelThresholds = getPixelThresholds(frame, axis); + + beforeEach(() => { + // avoiding any window scrolling + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, + }); + }); + describe('moving forward to end of droppable', () => { + const onStartBoundary: Position = patch( + axis.line, + // to the boundary is not enough to start + (frame[axis.size] - thresholds.startFrom), + frame.center[axis.crossLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onStartBoundary), scrollable) + ); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.line, 1)); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrollable), + ); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + // moving forwards + const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + + expect(id).toBe(scrollable.descriptor.id); + expect(scroll[axis.line]).toBeGreaterThan(0); + expect(scroll[axis.crossLine]).toBe(0); + }); + }); }); describe('window scrolling before droppable scrolling', () => { diff --git a/test/unit/state/auto-scroll/can-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js index 0c68bbc784..56c1e7c140 100644 --- a/test/unit/state/auto-scroll/can-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-scroll.spec.js @@ -36,6 +36,7 @@ const scrollable: DroppableDimension = getDroppableDimension({ top: 0, left: 0, right: 100, + // bigger than the frame bottom: 200, }), closest: { From cc45e7d87fbb8769973c92ad56cb7b68874be743 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 6 Feb 2018 17:16:11 +1100 Subject: [PATCH 060/163] droppable scroll tests --- .../state/auto-scroll/auto-scroller.spec.js | 315 +++++++++++++++++- 1 file changed, 309 insertions(+), 6 deletions(-) diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index e0e8980a56..29a2e238cc 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -20,7 +20,7 @@ import createAutoScroller from '../../../../src/state/auto-scroll/auto-scroller' import * as state from '../../../utils/simple-state-preset'; import { getPreset } from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; -import { getDraggableDimension, getDroppableDimension } from '../../../../src/state/dimension'; +import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; const addDroppable = (base: State, droppable: DroppableDimension): State => ({ ...base, @@ -88,6 +88,7 @@ describe('auto scroller', () => { describe('window scrolling', () => { const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); + describe('moving forward to end of window', () => { const onStartBoundary: Position = patch( axis.line, @@ -128,7 +129,7 @@ describe('auto scroller', () => { const target2: Position = add(onStartBoundary, patch(axis.line, 2)); autoScroller.onStateChange(state.idle, dragTo(target1)); - autoScroller.onStateChange(state.idle, dragTo(target2)); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); expect(mocks.scrollWindow).not.toHaveBeenCalled(); @@ -152,7 +153,7 @@ describe('auto scroller', () => { expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); - autoScroller.onStateChange(state.idle, dragTo(target2)); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); requestAnimationFrame.step(); expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); @@ -246,7 +247,7 @@ describe('auto scroller', () => { const onStartBoundary: Position = patch( axis.line, // at the boundary is not enough to start - windowScroll[axis.line] + (thresholds.startFrom), + windowScroll[axis.line] + thresholds.startFrom, viewport.center[axis.crossLine], ); const onMaxBoundary: Position = patch( @@ -282,7 +283,7 @@ describe('auto scroller', () => { const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); autoScroller.onStateChange(state.idle, dragTo(target1)); - autoScroller.onStateChange(state.idle, dragTo(target2)); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); expect(mocks.scrollWindow).not.toHaveBeenCalled(); @@ -306,7 +307,7 @@ describe('auto scroller', () => { expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); - autoScroller.onStateChange(state.idle, dragTo(target2)); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); requestAnimationFrame.step(); expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); @@ -522,6 +523,11 @@ describe('auto scroller', () => { (frame[axis.size] - thresholds.startFrom), frame.center[axis.crossLine], ); + const onMaxBoundary: Position = patch( + axis.line, + (frame[axis.size] - thresholds.maxSpeedAt), + frame.center[axis.crossLine], + ); it('should not scroll if not past the start threshold', () => { autoScroller.onStateChange( @@ -553,6 +559,303 @@ describe('auto scroller', () => { expect(scroll[axis.line]).toBeGreaterThan(0); expect(scroll[axis.crossLine]).toBe(0); }); + + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrollable), + ); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrollable), + addDroppable(dragTo(target2), scrollable), + ); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + + // not testing value called as we are not exposing getRequired scroll + }); + + it('should get faster the closer to the max speed point', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrollable), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); + + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrollable), + addDroppable(dragTo(target2), scrollable), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); + + expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); + + // validation + expect(scroll1[axis.crossLine]).toBe(0); + expect(scroll2[axis.crossLine]).toBe(0); + }); + + it('should have the top speed at the max speed point', () => { + const expected: Position = patch(axis.line, config.maxScrollSpeed); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onMaxBoundary), scrollable), + ); + requestAnimationFrame.step(); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); + + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = add(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, config.maxScrollSpeed); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrollable), + ); + requestAnimationFrame.step(); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); + + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, + }, + }, + }; + + return addDroppable(addDraggable(updated, tooBig), scrollable); + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); + + describe('moving backward to the start of droppable', () => { + const droppableScroll: Position = patch(axis.line, 10); + const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); + + const onStartBoundary: Position = patch( + axis.line, + // to the boundary is not enough to start + (frame[axis.start] + thresholds.startFrom), + frame.center[axis.crossLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (frame[axis.start] + thresholds.maxSpeedAt), + frame.center[axis.crossLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onStartBoundary), scrolled) + ); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + + it('should scroll if moving beyond the start threshold', () => { + // going backwards + const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), + ); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + + // validation + expect(id).toBe(scrollable.descriptor.id); + // moving backwards + expect(scroll[axis.line]).toBeLessThan(0); + expect(scroll[axis.crossLine]).toBe(0); + }); + + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrolled), + ); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrolled), + addDroppable(dragTo(target2), scrolled), + ); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + + // not testing value called as we are not exposing getRequired scroll + }); + + it('should get faster the closer to the max speed point', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrolled), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); + + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrolled), + addDroppable(dragTo(target2), scrolled), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); + + // moving backwards + expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); + + // validation + expect(scroll1[axis.crossLine]).toBe(0); + expect(scroll2[axis.crossLine]).toBe(0); + }); + + it('should have the top speed at the max speed point', () => { + const expected: Position = patch(axis.line, -config.maxScrollSpeed); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onMaxBoundary), scrolled), + ); + requestAnimationFrame.step(); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); + + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, -config.maxScrollSpeed); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), + ); + requestAnimationFrame.step(); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); + + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, + }, + }, + }; + + return addDroppable(addDraggable(updated, tooBig), scrolled); + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); }); }); From 7df62d0dbb02b2530a3252bf3a0a2c913b989320 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 08:03:35 +1100 Subject: [PATCH 061/163] more tests --- .../state/auto-scroll/auto-scroller.spec.js | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index 29a2e238cc..967e544cac 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -432,6 +432,7 @@ describe('auto scroller', () => { }); }); + // just some light tests to ensure that cross axis moving also works describe('moving backward on the cross axis', () => { const windowScroll: Position = patch(axis.crossLine, 10); beforeEach(() => { @@ -682,6 +683,24 @@ describe('auto scroller', () => { requestAnimationFrame.flush(); expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); + + it('should not scroll if the droppable is unable to be scrolled', () => { + const target: Position = onMaxBoundary; + if (!scrollable.viewport.closestScrollable) { + throw new Error('Invalid test setup'); + } + // scrolling to max scroll point + const maxChange: Position = scrollable.viewport.closestScrollable.scroll.max; + const scrolled: DroppableDimension = scrollDroppable(scrollable, maxChange); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled) + ); + requestAnimationFrame.flush(); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); }); describe('moving backward to the start of droppable', () => { @@ -857,6 +876,93 @@ describe('auto scroller', () => { expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); }); + + // just some light tests to ensure that cross axis moving also works + describe('moving forward on the cross axis', () => { + const droppableScroll: Position = patch(axis.crossLine, 10); + const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); + + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + frame, + axis === vertical ? horizontal : vertical, + ); + + const onStartBoundary: Position = patch( + axis.line, + frame.center[axis.line], + // to the boundary is not enough to start + (frame[axis.crossAxisSize] - crossAxisThresholds.startFrom), + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.crossLine, 1)); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), + ); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + // moving forwards + const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + + expect(id).toBe(scrolled.descriptor.id); + expect(scroll[axis.crossLine]).toBeGreaterThan(0); + }); + }); + + // just some light tests to ensure that cross axis moving also works + describe.skip('moving backward on the cross axis', () => { + const windowScroll: Position = patch(axis.crossLine, 10); + beforeEach(() => { + setWindowScroll(windowScroll); + }); + + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + viewport, + axis === vertical ? horizontal : vertical, + ); + + const onStartBoundary: Position = patch( + axis.line, + viewport.center[axis.line], + // to the boundary is not enough to start + windowScroll[axis.crossLine] + (crossAxisThresholds.startFrom) + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + + it('should scroll if moving beyond the start threshold', () => { + const target: Position = subtract(onStartBoundary, patch(axis.crossLine, 1)); + + autoScroller.onStateChange(state.idle, dragTo(target)); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving backwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.crossLine]).toBeLessThan(0); + }); + }); }); describe('window scrolling before droppable scrolling', () => { From 1226887c39eed45509d031e923334207beffb5f9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 08:09:34 +1100 Subject: [PATCH 062/163] more tests for cross axis droppable scrolling --- .../state/auto-scroll/auto-scroller.spec.js | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index 967e544cac..3193b5a236 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -875,6 +875,23 @@ describe('auto scroller', () => { requestAnimationFrame.flush(); expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); + + it('should not scroll if the droppable is unable to be scrolled', () => { + const target: Position = onMaxBoundary; + if (!scrollable.viewport.closestScrollable) { + throw new Error('Invalid test setup'); + } + // scrolling to max scroll point + + autoScroller.onStateChange( + state.idle, + // scrollable cannot be scrolled backwards + addDroppable(dragTo(target), scrollable) + ); + requestAnimationFrame.flush(); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); }); // just some light tests to ensure that cross axis moving also works @@ -923,12 +940,8 @@ describe('auto scroller', () => { }); // just some light tests to ensure that cross axis moving also works - describe.skip('moving backward on the cross axis', () => { - const windowScroll: Position = patch(axis.crossLine, 10); - beforeEach(() => { - setWindowScroll(windowScroll); - }); - + describe('moving backward on the cross axis', () => { + const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.crossLine, 10)); const crossAxisThresholds: PixelThresholds = getPixelThresholds( viewport, axis === vertical ? horizontal : vertical, @@ -936,30 +949,36 @@ describe('auto scroller', () => { const onStartBoundary: Position = patch( axis.line, - viewport.center[axis.line], + frame.center[axis.line], // to the boundary is not enough to start - windowScroll[axis.crossLine] + (crossAxisThresholds.startFrom) + (frame[axis.start] + thresholds.startFrom) ); it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onStartBoundary), scrolled), + ); requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); it('should scroll if moving beyond the start threshold', () => { const target: Position = subtract(onStartBoundary, patch(axis.crossLine, 1)); - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), + ); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); // only called after a frame requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); // moving backwards - const request: Position = mocks.scrollWindow.mock.calls[0][0]; + const request: Position = mocks.scrollDroppable.mock.calls[0][1]; expect(request[axis.crossLine]).toBeLessThan(0); }); }); From 535b1b555a07b271278ed1b540ed11a28b12bd55 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 08:12:12 +1100 Subject: [PATCH 063/163] renaming axis property --- src/state/axis.js | 4 +- src/state/move-to-edge.js | 2 +- src/types.js | 4 +- .../state/auto-scroll/auto-scroller.spec.js | 63 ++++++++++--------- test/unit/state/get-drag-impact.spec.js | 20 +++--- test/unit/state/move-to-edge.spec.js | 2 +- 6 files changed, 49 insertions(+), 46 deletions(-) diff --git a/src/state/axis.js b/src/state/axis.js index e3bdec7dbb..79e605efe6 100644 --- a/src/state/axis.js +++ b/src/state/axis.js @@ -4,7 +4,7 @@ import type { HorizontalAxis, VerticalAxis } from '../types'; export const vertical: VerticalAxis = { direction: 'vertical', line: 'y', - crossLine: 'x', + crossAxisLine: 'x', start: 'top', end: 'bottom', size: 'height', @@ -16,7 +16,7 @@ export const vertical: VerticalAxis = { export const horizontal: HorizontalAxis = { direction: 'horizontal', line: 'x', - crossLine: 'y', + crossAxisLine: 'y', start: 'left', end: 'right', size: 'width', diff --git a/src/state/move-to-edge.js b/src/state/move-to-edge.js index fbdd3a7e44..335bc5224b 100644 --- a/src/state/move-to-edge.js +++ b/src/state/move-to-edge.js @@ -55,7 +55,7 @@ export default ({ destinationAxis.line, // if moving to the end edge - we need to pull the source backwards (sourceEdge === 'end' ? -1 : 1) * centerDiff[destinationAxis.line], - centerDiff[destinationAxis.crossLine], + centerDiff[destinationAxis.crossAxisLine], ); return add(corner, signed); diff --git a/src/types.js b/src/types.js index ecce943b86..13d572790e 100644 --- a/src/types.js +++ b/src/types.js @@ -49,10 +49,10 @@ export type Direction = 'horizontal' | 'vertical'; export type VerticalAxis = {| direction: 'vertical', line: 'y', - crossLine: 'x', start: 'top', end: 'bottom', size: 'height', + crossAxisLine: 'x', crossAxisStart: 'left', crossAxisEnd: 'right', crossAxisSize: 'width', @@ -61,10 +61,10 @@ export type VerticalAxis = {| export type HorizontalAxis = {| direction: 'horizontal', line: 'x', - crossLine: 'y', start: 'left', end: 'right', size: 'width', + crossAxisLine: 'y', crossAxisStart: 'top', crossAxisEnd: 'bottom', crossAxisSize: 'height', diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index 3193b5a236..c5fdbe38c1 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -94,12 +94,12 @@ describe('auto scroller', () => { axis.line, // to the boundary is not enough to start (viewport[axis.size] - thresholds.startFrom), - viewport.center[axis.crossLine], + viewport.center[axis.crossAxisLine], ); const onMaxBoundary: Position = patch( axis.line, (viewport[axis.size] - thresholds.maxSpeedAt), - viewport.center[axis.crossLine], + viewport.center[axis.crossAxisLine], ); it('should not scroll if not past the start threshold', () => { @@ -161,8 +161,8 @@ describe('auto scroller', () => { expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); // validation - expect(scroll1[axis.crossLine]).toBe(0); - expect(scroll2[axis.crossLine]).toBe(0); + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); }); it('should have the top speed at the max speed point', () => { @@ -248,12 +248,12 @@ describe('auto scroller', () => { axis.line, // at the boundary is not enough to start windowScroll[axis.line] + thresholds.startFrom, - viewport.center[axis.crossLine], + viewport.center[axis.crossAxisLine], ); const onMaxBoundary: Position = patch( axis.line, (windowScroll[axis.line] + thresholds.maxSpeedAt), - viewport.center[axis.crossLine], + viewport.center[axis.crossAxisLine], ); it('should not scroll if not past the start threshold', () => { @@ -318,8 +318,8 @@ describe('auto scroller', () => { expect(Math.abs(scroll1[axis.line])).toBeLessThan(Math.abs(scroll2[axis.line])); // validation - expect(scroll1[axis.crossLine]).toBe(0); - expect(scroll2[axis.crossLine]).toBe(0); + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); }); it('should have the top speed at the max speed point', () => { @@ -417,7 +417,7 @@ describe('auto scroller', () => { }); it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.crossLine, 1)); + const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); autoScroller.onStateChange(state.idle, dragTo(target)); @@ -428,13 +428,13 @@ describe('auto scroller', () => { expect(mocks.scrollWindow).toHaveBeenCalled(); // moving forwards const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.crossLine]).toBeGreaterThan(0); + expect(request[axis.crossAxisLine]).toBeGreaterThan(0); }); }); // just some light tests to ensure that cross axis moving also works describe('moving backward on the cross axis', () => { - const windowScroll: Position = patch(axis.crossLine, 10); + const windowScroll: Position = patch(axis.crossAxisLine, 10); beforeEach(() => { setWindowScroll(windowScroll); }); @@ -448,7 +448,7 @@ describe('auto scroller', () => { axis.line, viewport.center[axis.line], // to the boundary is not enough to start - windowScroll[axis.crossLine] + (crossAxisThresholds.startFrom) + windowScroll[axis.crossAxisLine] + (crossAxisThresholds.startFrom) ); it('should not scroll if not past the start threshold', () => { @@ -459,7 +459,7 @@ describe('auto scroller', () => { }); it('should scroll if moving beyond the start threshold', () => { - const target: Position = subtract(onStartBoundary, patch(axis.crossLine, 1)); + const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); autoScroller.onStateChange(state.idle, dragTo(target)); @@ -470,7 +470,7 @@ describe('auto scroller', () => { expect(mocks.scrollWindow).toHaveBeenCalled(); // moving backwards const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.crossLine]).toBeLessThan(0); + expect(request[axis.crossAxisLine]).toBeLessThan(0); }); }); }); @@ -522,12 +522,12 @@ describe('auto scroller', () => { axis.line, // to the boundary is not enough to start (frame[axis.size] - thresholds.startFrom), - frame.center[axis.crossLine], + frame.center[axis.crossAxisLine], ); const onMaxBoundary: Position = patch( axis.line, (frame[axis.size] - thresholds.maxSpeedAt), - frame.center[axis.crossLine], + frame.center[axis.crossAxisLine], ); it('should not scroll if not past the start threshold', () => { @@ -558,7 +558,7 @@ describe('auto scroller', () => { expect(id).toBe(scrollable.descriptor.id); expect(scroll[axis.line]).toBeGreaterThan(0); - expect(scroll[axis.crossLine]).toBe(0); + expect(scroll[axis.crossAxisLine]).toBe(0); }); it('should throttle multiple scrolls into a single animation frame', () => { @@ -610,8 +610,8 @@ describe('auto scroller', () => { expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); // validation - expect(scroll1[axis.crossLine]).toBe(0); - expect(scroll2[axis.crossLine]).toBe(0); + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); }); it('should have the top speed at the max speed point', () => { @@ -711,12 +711,12 @@ describe('auto scroller', () => { axis.line, // to the boundary is not enough to start (frame[axis.start] + thresholds.startFrom), - frame.center[axis.crossLine], + frame.center[axis.crossAxisLine], ); const onMaxBoundary: Position = patch( axis.line, (frame[axis.start] + thresholds.maxSpeedAt), - frame.center[axis.crossLine], + frame.center[axis.crossAxisLine], ); it('should not scroll if not past the start threshold', () => { @@ -749,7 +749,7 @@ describe('auto scroller', () => { expect(id).toBe(scrollable.descriptor.id); // moving backwards expect(scroll[axis.line]).toBeLessThan(0); - expect(scroll[axis.crossLine]).toBe(0); + expect(scroll[axis.crossAxisLine]).toBe(0); }); it('should throttle multiple scrolls into a single animation frame', () => { @@ -802,8 +802,8 @@ describe('auto scroller', () => { expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); // validation - expect(scroll1[axis.crossLine]).toBe(0); - expect(scroll2[axis.crossLine]).toBe(0); + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); }); it('should have the top speed at the max speed point', () => { @@ -896,7 +896,7 @@ describe('auto scroller', () => { // just some light tests to ensure that cross axis moving also works describe('moving forward on the cross axis', () => { - const droppableScroll: Position = patch(axis.crossLine, 10); + const droppableScroll: Position = patch(axis.crossAxisLine, 10); const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); const crossAxisThresholds: PixelThresholds = getPixelThresholds( @@ -919,7 +919,7 @@ describe('auto scroller', () => { }); it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.crossLine, 1)); + const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); autoScroller.onStateChange( state.idle, @@ -935,13 +935,16 @@ describe('auto scroller', () => { const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; expect(id).toBe(scrolled.descriptor.id); - expect(scroll[axis.crossLine]).toBeGreaterThan(0); + expect(scroll[axis.crossAxisLine]).toBeGreaterThan(0); }); }); // just some light tests to ensure that cross axis moving also works describe('moving backward on the cross axis', () => { - const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.crossLine, 10)); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + patch(axis.crossAxisLine, 10) + ); const crossAxisThresholds: PixelThresholds = getPixelThresholds( viewport, axis === vertical ? horizontal : vertical, @@ -965,7 +968,7 @@ describe('auto scroller', () => { }); it('should scroll if moving beyond the start threshold', () => { - const target: Position = subtract(onStartBoundary, patch(axis.crossLine, 1)); + const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); autoScroller.onStateChange( state.idle, @@ -979,7 +982,7 @@ describe('auto scroller', () => { expect(mocks.scrollDroppable).toHaveBeenCalled(); // moving backwards const request: Position = mocks.scrollDroppable.mock.calls[0][1]; - expect(request[axis.crossLine]).toBeLessThan(0); + expect(request[axis.crossAxisLine]).toBeLessThan(0); }); }); }); diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index ec222beea1..a353c77cd0 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -120,7 +120,7 @@ describe('get drag impact', () => { // up to the line but not over it inHome2.page.withoutMargin[axis.start], // no movement on cross axis - inHome1.page.withoutMargin.center[axis.crossLine], + inHome1.page.withoutMargin.center[axis.crossAxisLine], ); const expected: DragImpact = { movement: { @@ -153,7 +153,7 @@ describe('get drag impact', () => { axis.line, inHome4.page.withoutMargin[axis.start] + 1, // no change - inHome2.page.withoutMargin.center[axis.crossLine], + inHome2.page.withoutMargin.center[axis.crossAxisLine], ); const expected: DragImpact = { movement: { @@ -199,7 +199,7 @@ describe('get drag impact', () => { axis.line, inHome1.page.withoutMargin[axis.end] - 1, // no change - inHome3.page.withoutMargin.center[axis.crossLine], + inHome3.page.withoutMargin.center[axis.crossAxisLine], ); const expected: DragImpact = { @@ -248,7 +248,7 @@ describe('get drag impact', () => { const startOfInHome2: Position = patch( axis.line, inHome2.page.withoutMargin[axis.start], - inHome2.page.withoutMargin.center[axis.crossLine], + inHome2.page.withoutMargin.center[axis.crossAxisLine], ); const distanceNeeded: Position = add( subtract(startOfInHome2, inHome1.page.withoutMargin.center), @@ -302,7 +302,7 @@ describe('get drag impact', () => { const endOfInHome2: Position = patch( axis.line, inHome2.page.withoutMargin[axis.end], - inHome2.page.withoutMargin.center[axis.crossLine], + inHome2.page.withoutMargin.center[axis.crossAxisLine], ); const distanceNeeded: Position = add( subtract(endOfInHome2, inHome4.page.withoutMargin.center), @@ -603,7 +603,7 @@ describe('get drag impact', () => { axis.line, // just before the end of the dimension which is the cut off inForeign1.page.withoutMargin[axis.end] - 1, - inForeign1.page.withoutMargin.center[axis.crossLine], + inForeign1.page.withoutMargin.center[axis.crossAxisLine], ); const expected: DragImpact = { movement: { @@ -660,7 +660,7 @@ describe('get drag impact', () => { const pageCenter: Position = patch( axis.line, inForeign2.page.withoutMargin[axis.end] - 1, - inForeign2.page.withoutMargin.center[axis.crossLine], + inForeign2.page.withoutMargin.center[axis.crossAxisLine], ); const expected: DragImpact = { movement: { @@ -712,7 +712,7 @@ describe('get drag impact', () => { const pageCenter: Position = patch( axis.line, inForeign4.page.withoutMargin[axis.end], - inForeign4.page.withoutMargin.center[axis.crossLine], + inForeign4.page.withoutMargin.center[axis.crossAxisLine], ); const expected: DragImpact = { movement: { @@ -777,7 +777,7 @@ describe('get drag impact', () => { const pageCenter: Position = patch( axis.line, inForeign2.page.withoutMargin[axis.end] - 1, - inForeign2.page.withoutMargin.center[axis.crossLine], + inForeign2.page.withoutMargin.center[axis.crossAxisLine], ); it('should have no impact impact the destination (actual)', () => { @@ -873,7 +873,7 @@ describe('get drag impact', () => { const pageCenter: Position = patch( axis.line, inForeign2.page.withoutMargin[axis.end] - 1, - inForeign2.page.withoutMargin.center[axis.crossLine], + inForeign2.page.withoutMargin.center[axis.crossAxisLine], ); it('should impact the destination (actual)', () => { diff --git a/test/unit/state/move-to-edge.spec.js b/test/unit/state/move-to-edge.spec.js index 55d60232bd..45314e5b09 100644 --- a/test/unit/state/move-to-edge.spec.js +++ b/test/unit/state/move-to-edge.spec.js @@ -46,7 +46,7 @@ const destination: Area = getArea({ const pullBackwardsOnMainAxis = (axis: Axis) => (point: Position) => patch( axis.line, -point[axis.line], - point[axis.crossLine] + point[axis.crossAxisLine] ); // returns the absolute difference of the center position From 314de426cbdd9e84dd839015c6f2e9c076b3da15 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 08:15:00 +1100 Subject: [PATCH 064/163] fixing test --- test/unit/state/auto-scroll/auto-scroller.spec.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index c5fdbe38c1..1e51f4d1a7 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -941,12 +941,10 @@ describe('auto scroller', () => { // just some light tests to ensure that cross axis moving also works describe('moving backward on the cross axis', () => { - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - patch(axis.crossAxisLine, 10) - ); + const droppableScroll: Position = patch(axis.crossAxisLine, 10); + const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); const crossAxisThresholds: PixelThresholds = getPixelThresholds( - viewport, + frame, axis === vertical ? horizontal : vertical, ); @@ -954,7 +952,7 @@ describe('auto scroller', () => { axis.line, frame.center[axis.line], // to the boundary is not enough to start - (frame[axis.start] + thresholds.startFrom) + (frame[axis.crossAxisStart] + crossAxisThresholds.startFrom) ); it('should not scroll if not past the start threshold', () => { From ea8d32da93fc81a177e604e740bb5bd0a47f867a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 08:21:27 +1100 Subject: [PATCH 065/163] structure --- test/unit/state/auto-scroll/auto-scroller.spec.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/auto-scroller.spec.js index 1e51f4d1a7..3ccacae6d1 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/auto-scroller.spec.js @@ -986,7 +986,13 @@ describe('auto scroller', () => { }); describe('window scrolling before droppable scrolling', () => { - // TODO: if window scrolling - do not droppable scroll + it('should scroll the window only if both the window and droppable can be scrolled', () => { + + }); + + it('should only scroll the window even if there is overlap', () => { + + }); }); }); @@ -1002,8 +1008,4 @@ describe('auto scroller', () => { }); }); }); - - describe('jump scrolling', () => { - - }); }); From 2bd732428990d03c5722c60f22366fe3f4c4df70 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 09:18:27 +1100 Subject: [PATCH 066/163] finalising fluid scroll tests --- src/state/auto-scroll/create-jump-scroller.js | 2 + ...croller.spec.js => fluid-scroller.spec.js} | 164 +++++++++++++----- .../state/auto-scroll/jump-scroller.spec.js | 23 +++ 3 files changed, 146 insertions(+), 43 deletions(-) rename test/unit/state/auto-scroll/{auto-scroller.spec.js => fluid-scroller.spec.js} (91%) create mode 100644 test/unit/state/auto-scroll/jump-scroller.spec.js diff --git a/src/state/auto-scroll/create-jump-scroller.js b/src/state/auto-scroll/create-jump-scroller.js index c733899b64..586b895a07 100644 --- a/src/state/auto-scroll/create-jump-scroller.js +++ b/src/state/auto-scroll/create-jump-scroller.js @@ -70,6 +70,8 @@ export default ({ const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; + // Unlike the fluid scroller we scroll the droppable first + // to prevent the item from moving outside of its container if (closestScrollable) { if (isTooBigToAutoScroll(closestScrollable.frame, draggable.page.withMargin)) { moveByOffset(state, request); diff --git a/test/unit/state/auto-scroll/auto-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js similarity index 91% rename from test/unit/state/auto-scroll/auto-scroller.spec.js rename to test/unit/state/auto-scroll/fluid-scroller.spec.js index 3ccacae6d1..72d01fc4f3 100644 --- a/test/unit/state/auto-scroll/auto-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -44,6 +44,42 @@ const addDraggable = (base: State, draggable: DraggableDimension): State => ({ }, }); +const windowScrollSize = { + scrollHeight: 2000, + scrollWidth: 1600, +}; + +const scrollableScrollSize = { + scrollWidth: 800, + scrollHeight: 800, +}; +const frame: Area = getArea({ + top: 0, + left: 0, + right: 600, + bottom: 600, +}); +const scrollable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'drop-1', + type: 'TYPE', + }, + client: getArea({ + top: 0, + left: 0, + // bigger than the frame + right: scrollableScrollSize.scrollWidth, + bottom: scrollableScrollSize.scrollHeight, + }), + closest: { + frameClient: frame, + scrollWidth: scrollableScrollSize.scrollWidth, + scrollHeight: scrollableScrollSize.scrollHeight, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, +}); + describe('auto scroller', () => { let autoScroller: AutoScroller; let mocks; @@ -74,10 +110,7 @@ describe('auto scroller', () => { beforeEach(() => { setViewport(viewport); - setWindowScrollSize({ - scrollHeight: 2000, - scrollWidth: 1600, - }); + setWindowScrollSize(windowScrollSize); }); [vertical, horizontal].forEach((axis: Axis) => { @@ -476,37 +509,6 @@ describe('auto scroller', () => { }); describe('droppable scrolling', () => { - const scrollableScrollSize = { - scrollWidth: 800, - scrollHeight: 800, - }; - const frame: Area = getArea({ - top: 0, - left: 0, - right: 600, - bottom: 600, - }); - const scrollable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'drop-1', - type: 'TYPE', - }, - client: getArea({ - top: 0, - left: 0, - // bigger than the frame - right: scrollableScrollSize.scrollWidth, - bottom: scrollableScrollSize.scrollHeight, - }), - closest: { - frameClient: frame, - scrollWidth: scrollableScrollSize.scrollWidth, - scrollHeight: scrollableScrollSize.scrollHeight, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); - const thresholds: PixelThresholds = getPixelThresholds(frame, axis); beforeEach(() => { @@ -986,23 +988,99 @@ describe('auto scroller', () => { }); describe('window scrolling before droppable scrolling', () => { - it('should scroll the window only if both the window and droppable can be scrolled', () => { - + const custom: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'scrollable that is similiar to the viewport', + type: 'TYPE', + }, + client: getArea({ + top: 0, + left: 0, + // bigger than the frame + right: windowScrollSize.scrollWidth, + bottom: windowScrollSize.scrollHeight, + }), + closest: { + frameClient: viewport, + scrollWidth: windowScrollSize.scrollWidth, + scrollHeight: windowScrollSize.scrollHeight, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); + const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); - it('should only scroll the window even if there is overlap', () => { + it('should scroll the window only if both the window and droppable can be scrolled', () => { + const onMaxBoundary: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onMaxBoundary), custom), + ); + requestAnimationFrame.step(); + + expect(mocks.scrollWindow).toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); }); - }); - describe('on drag end', () => { - it('should cancel any pending window scroll', () => { + describe('on drag end', () => { + const endDragStates = [ + state.idle, + state.dropAnimating(), + state.userCancel(), + state.dropComplete(), + ]; + + endDragStates.forEach((end: State) => { + it('should cancel any pending window scroll', () => { + const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); + const onMaxBoundary: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); - }); + autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); + + // frame not cleared + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - it('should cancel any pending droppable scroll', () => { + // should cancel the next frame + autoScroller.onStateChange(dragTo(onMaxBoundary), end); + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + + it('should cancel any pending droppable scroll', () => { + const thresholds: PixelThresholds = getPixelThresholds(frame, axis); + const onMaxBoundary: Position = patch( + axis.line, + (frame[axis.size] - thresholds.maxSpeedAt), + frame.center[axis.crossAxisLine], + ); + const drag: State = addDroppable(dragTo(onMaxBoundary), scrollable); + + autoScroller.onStateChange( + state.idle, + drag + ); + + // frame not cleared + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + + // should cancel the next frame + autoScroller.onStateChange(drag, end); + requestAnimationFrame.flush(); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); }); }); }); diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js new file mode 100644 index 0000000000..7d36553ab6 --- /dev/null +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -0,0 +1,23 @@ +// @flow + +describe('jump scroller', () => { + describe('window scrolling', () => { + + }); + + describe('droppable scrolling', () => { + it('should call "move" with the request if the draggable is too big', () => { + + }); + + it('should scroll the droppable by the request if it can be done entirely', () => { + + }); + + describe('playing with the window', () => { + it('should ') + }); + }); + + +}); \ No newline at end of file From f43502872b219745c267797e56c42592a0b84c7a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 10:03:18 +1100 Subject: [PATCH 067/163] cleaning up --- src/state/auto-scroll/create-jump-scroller.js | 67 +- .../state/auto-scroll/fluid-scroller.spec.js | 1583 ++++++++--------- .../state/auto-scroll/jump-scroller.spec.js | 59 +- 3 files changed, 883 insertions(+), 826 deletions(-) diff --git a/src/state/auto-scroll/create-jump-scroller.js b/src/state/auto-scroll/create-jump-scroller.js index 586b895a07..0369001529 100644 --- a/src/state/auto-scroll/create-jump-scroller.js +++ b/src/state/auto-scroll/create-jump-scroller.js @@ -70,59 +70,72 @@ export default ({ const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; - // Unlike the fluid scroller we scroll the droppable first - // to prevent the item from moving outside of its container + // 1. Is the draggable too big to auto scroll? + if (isTooBigToAutoScroll(getViewport(), draggable.page.withMargin)) { + moveByOffset(state, request); + return; + } + if (closestScrollable) { if (isTooBigToAutoScroll(closestScrollable.frame, draggable.page.withMargin)) { moveByOffset(state, request); return; } + } - if (canScrollDroppable(droppable, request)) { - // not scheduling - jump requests need to be performed instantly - - // if the window can also not be scrolled - adjust the item - if (!canScrollWindow(request)) { - const overlap: ?Position = getDroppableOverlap(droppable, request); + // 1. We scroll the droppable first if we can to avoid the draggable + // leaving the list - console.log('droppable overlap?', overlap); + if (canScrollDroppable(droppable, request)) { + const overlap: ?Position = getDroppableOverlap(droppable, request); + // Droppable can absorb the entire scroll request + if (!overlap) { + scrollDroppable(droppable.descriptor.id, request); + return; + } + // there is overlap - can the window absorb it? - if (overlap) { - console.warn('DROPPABLE OVERLAP', overlap); - moveByOffset(state, overlap); - } - } else { - console.log('can still scroll window'); - } + const canWindowScrollOverlap: boolean = canScrollWindow(overlap); - scrollDroppable(droppable.descriptor.id, request); + // window cannot absorb overlap: we need to move it + if (!canWindowScrollOverlap) { + moveByOffset(state, overlap); return; } - // can now check if we need to scroll the window - } + // how much can the window aborb? + const windowOverlap: ?Position = getWindowOverlap(overlap); - // Scroll the window if we can + // window can absorb all of the overlap + if (!windowOverlap) { + scrollWindow(overlap); + return; + } - if (isTooBigToAutoScroll(getViewport(), draggable.page.withMargin)) { - moveByOffset(state, request); + // window can only partially absorb overlap + // need to move the item by the remainder and scroll the window + moveByOffset(state, windowOverlap); + scrollWindow(overlap); return; } + // 2. Cannot scroll the droppable - can we scroll the window? + + // Cannot scroll the window at all if (!canScrollWindow(request)) { - console.warn('Jump scroll requested but it cannot be done by Droppable or the Window'); moveByOffset(state, request); return; } const overlap: ?Position = getWindowOverlap(request); - if (overlap) { - console.warn('WINDOW OVERLAP', overlap); - moveByOffset(state, overlap); + // Window can absorb the entire scroll + if (!overlap) { + scrollWindow(request); + return; } - // not scheduling - jump requests need to be performed instantly + moveByOffset(state, overlap); scrollWindow(request); }; diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index 72d01fc4f3..35cf4d2bc7 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -10,9 +10,9 @@ import type { import type { AutoScroller } from '../../../../src/state/auto-scroll/auto-scroller-types'; import type { PixelThresholds } from '../../../../src/state/auto-scroll/create-fluid-scroller'; import { getPixelThresholds, config } from '../../../../src/state/auto-scroll/create-fluid-scroller'; -import setViewport, { resetViewport } from '../../../utils/set-viewport'; import { add, patch, subtract } from '../../../../src/state/position'; import getArea from '../../../../src/state/get-area'; +import setViewport, { resetViewport } from '../../../utils/set-viewport'; import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; import { vertical, horizontal } from '../../../../src/state/axis'; @@ -44,11 +44,6 @@ const addDraggable = (base: State, draggable: DraggableDimension): State => ({ }, }); -const windowScrollSize = { - scrollHeight: 2000, - scrollWidth: 1600, -}; - const scrollableScrollSize = { scrollWidth: 800, scrollHeight: 800, @@ -80,6 +75,18 @@ const scrollable: DroppableDimension = getDroppableDimension({ }, }); +const windowScrollSize = { + scrollHeight: 2000, + scrollWidth: 1600, +}; + +const viewport: Area = getArea({ + top: 0, + left: 0, + right: 800, + bottom: 1000, +}); + describe('auto scroller', () => { let autoScroller: AutoScroller; let mocks; @@ -92,6 +99,12 @@ describe('auto scroller', () => { }; autoScroller = createAutoScroller(mocks); }); + + beforeEach(() => { + setViewport(viewport); + setWindowScrollSize(windowScrollSize); + }); + afterEach(() => { resetWindowScroll(); resetWindowScrollSize(); @@ -100,986 +113,972 @@ describe('auto scroller', () => { }); describe('fluid scrolling', () => { - describe('on drag', () => { - const viewport: Area = getArea({ - top: 0, - left: 0, - right: 800, - bottom: 1000, - }); - - beforeEach(() => { - setViewport(viewport); - setWindowScrollSize(windowScrollSize); - }); - - [vertical, horizontal].forEach((axis: Axis) => { - describe(`on the ${axis.direction} axis`, () => { - const preset = getPreset(axis); - const dragTo = (selection: Position): State => - state.dragging(preset.inHome1.descriptor.id, selection); - - describe('window scrolling', () => { - const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); - - describe('moving forward to end of window', () => { - const onStartBoundary: Position = patch( - axis.line, - // to the boundary is not enough to start - (viewport[axis.size] - thresholds.startFrom), - viewport.center[axis.crossAxisLine], - ); - const onMaxBoundary: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.maxSpeedAt), - viewport.center[axis.crossAxisLine], - ); - - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + [vertical, horizontal].forEach((axis: Axis) => { + describe(`on the ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const dragTo = (selection: Position): State => + state.dragging(preset.inHome1.descriptor.id, selection); + + describe('window scrolling', () => { + const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); + + describe('moving forward to end of window', () => { + const onStartBoundary: Position = patch( + axis.line, + // to the boundary is not enough to start + (viewport[axis.size] - thresholds.startFrom), + viewport.center[axis.crossAxisLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.line, 1)); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.line, 1)); + autoScroller.onStateChange(state.idle, dragTo(target)); - autoScroller.onStateChange(state.idle, dragTo(target)); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving forwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.line]).toBeGreaterThan(0); + }); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); - // moving forwards - const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.line]).toBeGreaterThan(0); - }); + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); - it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = add(onStartBoundary, patch(axis.line, 1)); - const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange(state.idle, dragTo(target1)); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); - autoScroller.onStateChange(state.idle, dragTo(target1)); - autoScroller.onStateChange(dragTo(target1), dragTo(target2)); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - // verification - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + // not testing value called as we are not exposing getRequired scroll + }); - // not testing value called as we are not exposing getRequired scroll - }); + it('should get faster the closer to the max speed point', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); - it('should get faster the closer to the max speed point', () => { - const target1: Position = add(onStartBoundary, patch(axis.line, 1)); - const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange(state.idle, dragTo(target1)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); - autoScroller.onStateChange(state.idle, dragTo(target1)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); - autoScroller.onStateChange(dragTo(target1), dragTo(target2)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); - const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); + expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); - expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); + // validation + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); + }); - // validation - expect(scroll1[axis.crossAxisLine]).toBe(0); - expect(scroll2[axis.crossAxisLine]).toBe(0); - }); + it('should have the top speed at the max speed point', () => { + const expected: Position = patch(axis.line, config.maxScrollSpeed); - it('should have the top speed at the max speed point', () => { - const expected: Position = patch(axis.line, config.maxScrollSpeed); + autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); - requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); - expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); - }); + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = add(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, config.maxScrollSpeed); - it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = add(onMaxBoundary, patch(axis.line, 1)); - const expected: Position = patch(axis.line, config.maxScrollSpeed); + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(target)); - requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); - expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); - it('should not scroll if the item is too big', () => { - const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBig.descriptor, - }, + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, }, - }; + }, + }; - return addDraggable(updated, tooBig); - })(); + return addDraggable(updated, tooBig); + })(); - autoScroller.onStateChange(state.idle, custom); + autoScroller.onStateChange(state.idle, custom); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - it('should not scroll if the window cannot scroll', () => { - setWindowScrollSize({ - scrollHeight: viewport.height, - scrollWidth: viewport.width, - }); - const target: Position = onMaxBoundary; + it('should not scroll if the window cannot scroll', () => { + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, + }); + const target: Position = onMaxBoundary; - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange(state.idle, dragTo(target)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); + }); - describe('moving backwards towards the start of window', () => { - const windowScroll: Position = patch(axis.line, 10); + describe('moving backwards towards the start of window', () => { + const windowScroll: Position = patch(axis.line, 10); - beforeEach(() => { - setWindowScroll(windowScroll); - }); + beforeEach(() => { + setWindowScroll(windowScroll); + }); - const onStartBoundary: Position = patch( - axis.line, - // at the boundary is not enough to start - windowScroll[axis.line] + thresholds.startFrom, - viewport.center[axis.crossAxisLine], - ); - const onMaxBoundary: Position = patch( - axis.line, - (windowScroll[axis.line] + thresholds.maxSpeedAt), - viewport.center[axis.crossAxisLine], - ); + const onStartBoundary: Position = patch( + axis.line, + // at the boundary is not enough to start + windowScroll[axis.line] + thresholds.startFrom, + viewport.center[axis.crossAxisLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + autoScroller.onStateChange(state.idle, dragTo(target)); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - autoScroller.onStateChange(state.idle, dragTo(target)); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving backwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.line]).toBeLessThan(0); + }); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); - // moving backwards - const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.line]).toBeLessThan(0); - }); + autoScroller.onStateChange(state.idle, dragTo(target1)); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); - it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); - const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - autoScroller.onStateChange(state.idle, dragTo(target1)); - autoScroller.onStateChange(dragTo(target1), dragTo(target2)); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + // not testing value called as we are not exposing getRequired scroll + }); - // verification - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + it('should get faster the closer to the max speed point', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); - // not testing value called as we are not exposing getRequired scroll - }); + autoScroller.onStateChange(state.idle, dragTo(target1)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); - it('should get faster the closer to the max speed point', () => { - const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); - const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); - autoScroller.onStateChange(state.idle, dragTo(target1)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); + // moving backwards so a smaller value is bigger + expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); + // or put another way: + expect(Math.abs(scroll1[axis.line])).toBeLessThan(Math.abs(scroll2[axis.line])); - autoScroller.onStateChange(dragTo(target1), dragTo(target2)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); - const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); + // validation + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); + }); - // moving backwards so a smaller value is bigger - expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); - // or put another way: - expect(Math.abs(scroll1[axis.line])).toBeLessThan(Math.abs(scroll2[axis.line])); + it('should have the top speed at the max speed point', () => { + const target: Position = onMaxBoundary; + const expected: Position = patch(axis.line, -config.maxScrollSpeed); - // validation - expect(scroll1[axis.crossAxisLine]).toBe(0); - expect(scroll2[axis.crossAxisLine]).toBe(0); - }); - - it('should have the top speed at the max speed point', () => { - const target: Position = onMaxBoundary; - const expected: Position = patch(axis.line, -config.maxScrollSpeed); + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(target)); - requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); - expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); - }); + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, -config.maxScrollSpeed); - it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); - const expected: Position = patch(axis.line, -config.maxScrollSpeed); + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(target)); - requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); - expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); - it('should not scroll if the item is too big', () => { - const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBig.descriptor, - }, + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, }, - }; - - return addDraggable(updated, tooBig); - })(); - - autoScroller.onStateChange(state.idle, custom); - - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + }, + }; - it('should not scroll if the window cannot scroll', () => { - setWindowScrollSize({ - scrollHeight: viewport.height, - scrollWidth: viewport.width, - }); - const target: Position = onMaxBoundary; + return addDraggable(updated, tooBig); + })(); - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange(state.idle, custom); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); - // just some light tests to ensure that cross axis moving also works - describe('moving forward on the cross axis', () => { - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - viewport, - axis === vertical ? horizontal : vertical, - ); + it('should not scroll if the window cannot scroll', () => { + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, + }); + const target: Position = onMaxBoundary; - const onStartBoundary: Position = patch( - axis.line, - viewport.center[axis.line], - // to the boundary is not enough to start - (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom), - ); + autoScroller.onStateChange(state.idle, dragTo(target)); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + // just some light tests to ensure that cross axis moving also works + describe('moving forward on the cross axis', () => { + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + viewport, + axis === vertical ? horizontal : vertical, + ); + + const onStartBoundary: Position = patch( + axis.line, + viewport.center[axis.line], + // to the boundary is not enough to start + (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom), + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange(state.idle, dragTo(target)); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); - // moving forwards - const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.crossAxisLine]).toBeGreaterThan(0); - }); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving forwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.crossAxisLine]).toBeGreaterThan(0); }); + }); - // just some light tests to ensure that cross axis moving also works - describe('moving backward on the cross axis', () => { - const windowScroll: Position = patch(axis.crossAxisLine, 10); - beforeEach(() => { - setWindowScroll(windowScroll); - }); + // just some light tests to ensure that cross axis moving also works + describe('moving backward on the cross axis', () => { + const windowScroll: Position = patch(axis.crossAxisLine, 10); + beforeEach(() => { + setWindowScroll(windowScroll); + }); - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - viewport, - axis === vertical ? horizontal : vertical, - ); + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + viewport, + axis === vertical ? horizontal : vertical, + ); - const onStartBoundary: Position = patch( - axis.line, - viewport.center[axis.line], - // to the boundary is not enough to start - windowScroll[axis.crossAxisLine] + (crossAxisThresholds.startFrom) - ); + const onStartBoundary: Position = patch( + axis.line, + viewport.center[axis.line], + // to the boundary is not enough to start + windowScroll[axis.crossAxisLine] + (crossAxisThresholds.startFrom) + ); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange(state.idle, dragTo(target)); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); - // moving backwards - const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.crossAxisLine]).toBeLessThan(0); - }); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving backwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.crossAxisLine]).toBeLessThan(0); }); }); + }); - describe('droppable scrolling', () => { - const thresholds: PixelThresholds = getPixelThresholds(frame, axis); + describe('droppable scrolling', () => { + const thresholds: PixelThresholds = getPixelThresholds(frame, axis); - beforeEach(() => { - // avoiding any window scrolling - setWindowScrollSize({ - scrollHeight: viewport.height, - scrollWidth: viewport.width, - }); + beforeEach(() => { + // avoiding any window scrolling + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, }); + }); - describe('moving forward to end of droppable', () => { - const onStartBoundary: Position = patch( - axis.line, - // to the boundary is not enough to start - (frame[axis.size] - thresholds.startFrom), - frame.center[axis.crossAxisLine], - ); - const onMaxBoundary: Position = patch( - axis.line, - (frame[axis.size] - thresholds.maxSpeedAt), - frame.center[axis.crossAxisLine], + describe('moving forward to end of droppable', () => { + const onStartBoundary: Position = patch( + axis.line, + // to the boundary is not enough to start + (frame[axis.size] - thresholds.startFrom), + frame.center[axis.crossAxisLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (frame[axis.size] - thresholds.maxSpeedAt), + frame.center[axis.crossAxisLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onStartBoundary), scrollable) ); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onStartBoundary), scrollable) - ); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.line, 1)); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.line, 1)); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrollable), + ); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrollable), - ); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + // moving forwards + const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalled(); - // moving forwards - const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + expect(id).toBe(scrollable.descriptor.id); + expect(scroll[axis.line]).toBeGreaterThan(0); + expect(scroll[axis.crossAxisLine]).toBe(0); + }); - expect(id).toBe(scrollable.descriptor.id); - expect(scroll[axis.line]).toBeGreaterThan(0); - expect(scroll[axis.crossAxisLine]).toBe(0); - }); + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); - it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = add(onStartBoundary, patch(axis.line, 1)); - const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrollable), + ); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrollable), + addDroppable(dragTo(target2), scrollable), + ); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target1), scrollable), - ); - autoScroller.onStateChange( - addDroppable(dragTo(target1), scrollable), - addDroppable(dragTo(target2), scrollable), - ); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - // verification - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + // not testing value called as we are not exposing getRequired scroll + }); - // not testing value called as we are not exposing getRequired scroll - }); + it('should get faster the closer to the max speed point', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); - it('should get faster the closer to the max speed point', () => { - const target1: Position = add(onStartBoundary, patch(axis.line, 1)); - const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrollable), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target1), scrollable), - ); - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrollable), + addDroppable(dragTo(target2), scrollable), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); - autoScroller.onStateChange( - addDroppable(dragTo(target1), scrollable), - addDroppable(dragTo(target2), scrollable), - ); - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); - const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); + expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); - expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); + // validation + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); + }); - // validation - expect(scroll1[axis.crossAxisLine]).toBe(0); - expect(scroll2[axis.crossAxisLine]).toBe(0); - }); + it('should have the top speed at the max speed point', () => { + const expected: Position = patch(axis.line, config.maxScrollSpeed); - it('should have the top speed at the max speed point', () => { - const expected: Position = patch(axis.line, config.maxScrollSpeed); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onMaxBoundary), scrollable), + ); + requestAnimationFrame.step(); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onMaxBoundary), scrollable), - ); - requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - expected - ); - }); + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = add(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, config.maxScrollSpeed); - it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = add(onMaxBoundary, patch(axis.line, 1)); - const expected: Position = patch(axis.line, config.maxScrollSpeed); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrollable), + ); + requestAnimationFrame.step(); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrollable), - ); - requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - expected - ); + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); - it('should not scroll if the item is too big', () => { - const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBig.descriptor, - }, + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, }, - }; - - return addDroppable(addDraggable(updated, tooBig), scrollable); - })(); - - autoScroller.onStateChange(state.idle, custom); + }, + }; - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + return addDroppable(addDraggable(updated, tooBig), scrollable); + })(); - it('should not scroll if the droppable is unable to be scrolled', () => { - const target: Position = onMaxBoundary; - if (!scrollable.viewport.closestScrollable) { - throw new Error('Invalid test setup'); - } - // scrolling to max scroll point - const maxChange: Position = scrollable.viewport.closestScrollable.scroll.max; - const scrolled: DroppableDimension = scrollDroppable(scrollable, maxChange); - - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled) - ); - requestAnimationFrame.flush(); + autoScroller.onStateChange(state.idle, custom); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); - describe('moving backward to the start of droppable', () => { - const droppableScroll: Position = patch(axis.line, 10); - const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); + it('should not scroll if the droppable is unable to be scrolled', () => { + const target: Position = onMaxBoundary; + if (!scrollable.viewport.closestScrollable) { + throw new Error('Invalid test setup'); + } + // scrolling to max scroll point + const maxChange: Position = scrollable.viewport.closestScrollable.scroll.max; + const scrolled: DroppableDimension = scrollDroppable(scrollable, maxChange); - const onStartBoundary: Position = patch( - axis.line, - // to the boundary is not enough to start - (frame[axis.start] + thresholds.startFrom), - frame.center[axis.crossAxisLine], - ); - const onMaxBoundary: Position = patch( - axis.line, - (frame[axis.start] + thresholds.maxSpeedAt), - frame.center[axis.crossAxisLine], + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled) ); + requestAnimationFrame.flush(); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onStartBoundary), scrolled) - ); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + describe('moving backward to the start of droppable', () => { + const droppableScroll: Position = patch(axis.line, 10); + const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); + + const onStartBoundary: Position = patch( + axis.line, + // to the boundary is not enough to start + (frame[axis.start] + thresholds.startFrom), + frame.center[axis.crossAxisLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (frame[axis.start] + thresholds.maxSpeedAt), + frame.center[axis.crossAxisLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onStartBoundary), scrolled) + ); - it('should scroll if moving beyond the start threshold', () => { - // going backwards - const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled), - ); + it('should scroll if moving beyond the start threshold', () => { + // going backwards + const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), + ); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalled(); - const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - // validation - expect(id).toBe(scrollable.descriptor.id); - // moving backwards - expect(scroll[axis.line]).toBeLessThan(0); - expect(scroll[axis.crossAxisLine]).toBe(0); - }); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + + // validation + expect(id).toBe(scrollable.descriptor.id); + // moving backwards + expect(scroll[axis.line]).toBeLessThan(0); + expect(scroll[axis.crossAxisLine]).toBe(0); + }); - it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); - const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target1), scrolled), - ); - autoScroller.onStateChange( - addDroppable(dragTo(target1), scrolled), - addDroppable(dragTo(target2), scrolled), - ); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrolled), + ); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrolled), + addDroppable(dragTo(target2), scrolled), + ); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - // verification - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - // not testing value called as we are not exposing getRequired scroll - }); + // not testing value called as we are not exposing getRequired scroll + }); - it('should get faster the closer to the max speed point', () => { - const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); - const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + it('should get faster the closer to the max speed point', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target1), scrolled), - ); - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrolled), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); - autoScroller.onStateChange( - addDroppable(dragTo(target1), scrolled), - addDroppable(dragTo(target2), scrolled), - ); - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); - const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrolled), + addDroppable(dragTo(target2), scrolled), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); - // moving backwards - expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); + // moving backwards + expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); - // validation - expect(scroll1[axis.crossAxisLine]).toBe(0); - expect(scroll2[axis.crossAxisLine]).toBe(0); - }); + // validation + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); + }); - it('should have the top speed at the max speed point', () => { - const expected: Position = patch(axis.line, -config.maxScrollSpeed); + it('should have the top speed at the max speed point', () => { + const expected: Position = patch(axis.line, -config.maxScrollSpeed); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onMaxBoundary), scrolled), - ); - requestAnimationFrame.step(); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onMaxBoundary), scrolled), + ); + requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - expected - ); - }); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); - it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); - const expected: Position = patch(axis.line, -config.maxScrollSpeed); + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, -config.maxScrollSpeed); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled), - ); - requestAnimationFrame.step(); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), + ); + requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - expected - ); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); + + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); - it('should not scroll if the item is too big', () => { - const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBig.descriptor, - }, + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, }, - }; - - return addDroppable(addDraggable(updated, tooBig), scrolled); - })(); - - autoScroller.onStateChange(state.idle, custom); + }, + }; - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + return addDroppable(addDraggable(updated, tooBig), scrolled); + })(); - it('should not scroll if the droppable is unable to be scrolled', () => { - const target: Position = onMaxBoundary; - if (!scrollable.viewport.closestScrollable) { - throw new Error('Invalid test setup'); - } - // scrolling to max scroll point - - autoScroller.onStateChange( - state.idle, - // scrollable cannot be scrolled backwards - addDroppable(dragTo(target), scrollable) - ); - requestAnimationFrame.flush(); + autoScroller.onStateChange(state.idle, custom); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); - // just some light tests to ensure that cross axis moving also works - describe('moving forward on the cross axis', () => { - const droppableScroll: Position = patch(axis.crossAxisLine, 10); - const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); - - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - frame, - axis === vertical ? horizontal : vertical, - ); + it('should not scroll if the droppable is unable to be scrolled', () => { + const target: Position = onMaxBoundary; + if (!scrollable.viewport.closestScrollable) { + throw new Error('Invalid test setup'); + } + // scrolling to max scroll point - const onStartBoundary: Position = patch( - axis.line, - frame.center[axis.line], - // to the boundary is not enough to start - (frame[axis.crossAxisSize] - crossAxisThresholds.startFrom), + autoScroller.onStateChange( + state.idle, + // scrollable cannot be scrolled backwards + addDroppable(dragTo(target), scrollable) ); + requestAnimationFrame.flush(); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + // just some light tests to ensure that cross axis moving also works + describe('moving forward on the cross axis', () => { + const droppableScroll: Position = patch(axis.crossAxisLine, 10); + const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + frame, + axis === vertical ? horizontal : vertical, + ); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled), - ); + const onStartBoundary: Position = patch( + axis.line, + frame.center[axis.line], + // to the boundary is not enough to start + (frame[axis.crossAxisSize] - crossAxisThresholds.startFrom), + ); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalled(); - // moving forwards - const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; - - expect(id).toBe(scrolled.descriptor.id); - expect(scroll[axis.crossAxisLine]).toBeGreaterThan(0); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); - // just some light tests to ensure that cross axis moving also works - describe('moving backward on the cross axis', () => { - const droppableScroll: Position = patch(axis.crossAxisLine, 10); - const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - frame, - axis === vertical ? horizontal : vertical, - ); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); - const onStartBoundary: Position = patch( - axis.line, - frame.center[axis.line], - // to the boundary is not enough to start - (frame[axis.crossAxisStart] + crossAxisThresholds.startFrom) + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), ); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onStartBoundary), scrolled), - ); - - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); - - it('should scroll if moving beyond the start threshold', () => { - const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); - - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled), - ); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + // moving forwards + const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalled(); - // moving backwards - const request: Position = mocks.scrollDroppable.mock.calls[0][1]; - expect(request[axis.crossAxisLine]).toBeLessThan(0); - }); + expect(id).toBe(scrolled.descriptor.id); + expect(scroll[axis.crossAxisLine]).toBeGreaterThan(0); }); }); - describe('window scrolling before droppable scrolling', () => { - const custom: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'scrollable that is similiar to the viewport', - type: 'TYPE', - }, - client: getArea({ - top: 0, - left: 0, - // bigger than the frame - right: windowScrollSize.scrollWidth, - bottom: windowScrollSize.scrollHeight, - }), - closest: { - frameClient: viewport, - scrollWidth: windowScrollSize.scrollWidth, - scrollHeight: windowScrollSize.scrollHeight, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, + // just some light tests to ensure that cross axis moving also works + describe('moving backward on the cross axis', () => { + const droppableScroll: Position = patch(axis.crossAxisLine, 10); + const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + frame, + axis === vertical ? horizontal : vertical, + ); + + const onStartBoundary: Position = patch( + axis.line, + frame.center[axis.line], + // to the boundary is not enough to start + (frame[axis.crossAxisStart] + crossAxisThresholds.startFrom) + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onStartBoundary), scrolled), + ); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); - const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); - it('should scroll the window only if both the window and droppable can be scrolled', () => { - const onMaxBoundary: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.maxSpeedAt), - viewport.center[axis.crossAxisLine], - ); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); autoScroller.onStateChange( state.idle, - addDroppable(dragTo(onMaxBoundary), custom), + addDroppable(dragTo(target), scrolled), ); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + // moving backwards + const request: Position = mocks.scrollDroppable.mock.calls[0][1]; + expect(request[axis.crossAxisLine]).toBeLessThan(0); }); }); + }); + + describe('window scrolling before droppable scrolling', () => { + const custom: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'scrollable that is similiar to the viewport', + type: 'TYPE', + }, + client: getArea({ + top: 0, + left: 0, + // bigger than the frame + right: windowScrollSize.scrollWidth, + bottom: windowScrollSize.scrollHeight, + }), + closest: { + frameClient: viewport, + scrollWidth: windowScrollSize.scrollWidth, + scrollHeight: windowScrollSize.scrollHeight, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); - describe('on drag end', () => { - const endDragStates = [ + it('should scroll the window only if both the window and droppable can be scrolled', () => { + const onMaxBoundary: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); + + autoScroller.onStateChange( state.idle, - state.dropAnimating(), - state.userCancel(), - state.dropComplete(), - ]; - - endDragStates.forEach((end: State) => { - it('should cancel any pending window scroll', () => { - const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); - const onMaxBoundary: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.maxSpeedAt), - viewport.center[axis.crossAxisLine], - ); + addDroppable(dragTo(onMaxBoundary), custom), + ); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); + expect(mocks.scrollWindow).toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); - // frame not cleared - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + describe('on drag end', () => { + const endDragStates = [ + state.idle, + state.dropAnimating(), + state.userCancel(), + state.dropComplete(), + ]; + + endDragStates.forEach((end: State) => { + it('should cancel any pending window scroll', () => { + const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); + const onMaxBoundary: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); - // should cancel the next frame - autoScroller.onStateChange(dragTo(onMaxBoundary), end); - requestAnimationFrame.flush(); + autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + // frame not cleared + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - it('should cancel any pending droppable scroll', () => { - const thresholds: PixelThresholds = getPixelThresholds(frame, axis); - const onMaxBoundary: Position = patch( - axis.line, - (frame[axis.size] - thresholds.maxSpeedAt), - frame.center[axis.crossAxisLine], - ); - const drag: State = addDroppable(dragTo(onMaxBoundary), scrollable); + // should cancel the next frame + autoScroller.onStateChange(dragTo(onMaxBoundary), end); + requestAnimationFrame.flush(); - autoScroller.onStateChange( - state.idle, - drag - ); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - // frame not cleared - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + it('should cancel any pending droppable scroll', () => { + const thresholds: PixelThresholds = getPixelThresholds(frame, axis); + const onMaxBoundary: Position = patch( + axis.line, + (frame[axis.size] - thresholds.maxSpeedAt), + frame.center[axis.crossAxisLine], + ); + const drag: State = addDroppable(dragTo(onMaxBoundary), scrollable); + + autoScroller.onStateChange( + state.idle, + drag + ); - // should cancel the next frame - autoScroller.onStateChange(drag, end); - requestAnimationFrame.flush(); + // frame not cleared + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + // should cancel the next frame + autoScroller.onStateChange(drag, end); + requestAnimationFrame.flush(); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); }); }); diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index 7d36553ab6..2aa70a0bb8 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -1,23 +1,68 @@ // @flow +import type { + Area, + Axis, + Position, + State, + DraggableDimension, + DroppableDimension, +} from '../../../../src/types'; +import type { AutoScroller } from '../../../../src/state/auto-scroll/auto-scroller-types'; +import type { PixelThresholds } from '../../../../src/state/auto-scroll/create-fluid-scroller'; +import { getPixelThresholds, config } from '../../../../src/state/auto-scroll/create-fluid-scroller'; +import { add, patch, subtract } from '../../../../src/state/position'; +import getArea from '../../../../src/state/get-area'; +import setViewport, { resetViewport } from '../../../utils/set-viewport'; +import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; +import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; +import { vertical, horizontal } from '../../../../src/state/axis'; +import createAutoScroller from '../../../../src/state/auto-scroll/auto-scroller'; +import * as state from '../../../utils/simple-state-preset'; +import { getPreset } from '../../../utils/dimension'; +import { expandByPosition } from '../../../../src/state/spacing'; +import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; + +const windowScrollSize = { + scrollHeight: 2000, + scrollWidth: 1600, +}; + describe('jump scroller', () => { - describe('window scrolling', () => { + let autoScroller: AutoScroller; + let mocks; + + beforeEach(() => { + mocks = { + scrollWindow: jest.fn(), + scrollDroppable: jest.fn(), + move: jest.fn(), + }; + autoScroller = createAutoScroller(mocks); + }); + afterEach(() => { + resetWindowScroll(); + resetWindowScrollSize(); + resetViewport(); + requestAnimationFrame.reset(); }); - describe('droppable scrolling', () => { - it('should call "move" with the request if the draggable is too big', () => { + describe('window scrolling', () => { + it('should not scroll if the item is bigger than the viewport', () => { }); - it('should scroll the droppable by the request if it can be done entirely', () => { + it('should manually move the item if the window is unable to scroll', () => { }); - describe('playing with the window', () => { - it('should ') + it('should scroll the window if can absorb all of the movement', () => { + }); - }); + it('should manually move the item any distance that the window is unable to scroll', () => { + }); + }); }); \ No newline at end of file From 08bb3c155cec83918b945590c38bca46551ec989 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 11:11:36 +1100 Subject: [PATCH 068/163] jump scroller tests --- src/state/auto-scroll/auto-scroller.js | 1 + src/state/auto-scroll/create-jump-scroller.js | 13 +- .../state/auto-scroll/fluid-scroller.spec.js | 1552 ++++++++--------- .../state/auto-scroll/jump-scroller.spec.js | 95 +- test/utils/simple-state-preset.js | 57 + 5 files changed, 925 insertions(+), 793 deletions(-) diff --git a/src/state/auto-scroll/auto-scroller.js b/src/state/auto-scroll/auto-scroller.js index 09795a093d..ad093f63cd 100644 --- a/src/state/auto-scroll/auto-scroller.js +++ b/src/state/auto-scroll/auto-scroller.js @@ -56,6 +56,7 @@ export default ({ } jumpScroll(current); + return; } // cancel any pending scrolls if no longer dragging diff --git a/src/state/auto-scroll/create-jump-scroller.js b/src/state/auto-scroll/create-jump-scroller.js index 0369001529..96f98ffcb8 100644 --- a/src/state/auto-scroll/create-jump-scroller.js +++ b/src/state/auto-scroll/create-jump-scroller.js @@ -1,5 +1,5 @@ // @flow -import { add } from '../position'; +import { add, subtract } from '../position'; import getWindowScrollPosition from '../../window/get-window-scroll'; import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; import getViewport from '../../window/get-viewport'; @@ -24,7 +24,12 @@ import type { type Args = {| scrollDroppable: (id: DroppableId, offset: Position) => void, scrollWindow: (offset: Position) => void, - move: (id: DraggableId, client: Position, windowScroll: Position, shouldAnimate?: boolean) => void, + move: ( + id: DraggableId, + client: Position, + windowScroll: Position, + shouldAnimate?: boolean + ) => void, |} export type JumpScroller = (state: State) => void; @@ -136,7 +141,9 @@ export default ({ } moveByOffset(state, overlap); - scrollWindow(request); + // the amount that the window can actually scroll + const canScroll: Position = subtract(request, overlap); + scrollWindow(canScroll); }; return jumpScroller; diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index 35cf4d2bc7..2622506164 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -79,7 +79,6 @@ const windowScrollSize = { scrollHeight: 2000, scrollWidth: 1600, }; - const viewport: Area = getArea({ top: 0, left: 0, @@ -87,7 +86,7 @@ const viewport: Area = getArea({ bottom: 1000, }); -describe('auto scroller', () => { +describe('fluid auto scrolling', () => { let autoScroller: AutoScroller; let mocks; @@ -98,9 +97,6 @@ describe('auto scroller', () => { move: jest.fn(), }; autoScroller = createAutoScroller(mocks); - }); - - beforeEach(() => { setViewport(viewport); setWindowScrollSize(windowScrollSize); }); @@ -112,974 +108,972 @@ describe('auto scroller', () => { requestAnimationFrame.reset(); }); - describe('fluid scrolling', () => { - [vertical, horizontal].forEach((axis: Axis) => { - describe(`on the ${axis.direction} axis`, () => { - const preset = getPreset(axis); - const dragTo = (selection: Position): State => - state.dragging(preset.inHome1.descriptor.id, selection); - - describe('window scrolling', () => { - const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); - - describe('moving forward to end of window', () => { - const onStartBoundary: Position = patch( - axis.line, - // to the boundary is not enough to start - (viewport[axis.size] - thresholds.startFrom), - viewport.center[axis.crossAxisLine], - ); - const onMaxBoundary: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.maxSpeedAt), - viewport.center[axis.crossAxisLine], - ); - - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + [vertical, horizontal].forEach((axis: Axis) => { + describe(`on the ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const dragTo = (selection: Position): State => + state.dragging(preset.inHome1.descriptor.id, selection); + + describe('window scrolling', () => { + const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); + + describe('moving forward to end of window', () => { + const onStartBoundary: Position = patch( + axis.line, + // to the boundary is not enough to start + (viewport[axis.size] - thresholds.startFrom), + viewport.center[axis.crossAxisLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.line, 1)); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.line, 1)); + autoScroller.onStateChange(state.idle, dragTo(target)); - autoScroller.onStateChange(state.idle, dragTo(target)); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving forwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.line]).toBeGreaterThan(0); + }); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); - // moving forwards - const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.line]).toBeGreaterThan(0); - }); + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); - it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = add(onStartBoundary, patch(axis.line, 1)); - const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange(state.idle, dragTo(target1)); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); - autoScroller.onStateChange(state.idle, dragTo(target1)); - autoScroller.onStateChange(dragTo(target1), dragTo(target2)); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - // verification - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + // not testing value called as we are not exposing getRequired scroll + }); - // not testing value called as we are not exposing getRequired scroll - }); + it('should get faster the closer to the max speed point', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); - it('should get faster the closer to the max speed point', () => { - const target1: Position = add(onStartBoundary, patch(axis.line, 1)); - const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange(state.idle, dragTo(target1)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); - autoScroller.onStateChange(state.idle, dragTo(target1)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); - autoScroller.onStateChange(dragTo(target1), dragTo(target2)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); - const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); + expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); - expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); + // validation + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); + }); - // validation - expect(scroll1[axis.crossAxisLine]).toBe(0); - expect(scroll2[axis.crossAxisLine]).toBe(0); - }); + it('should have the top speed at the max speed point', () => { + const expected: Position = patch(axis.line, config.maxScrollSpeed); - it('should have the top speed at the max speed point', () => { - const expected: Position = patch(axis.line, config.maxScrollSpeed); + autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); - requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); - expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); - }); + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = add(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, config.maxScrollSpeed); - it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = add(onMaxBoundary, patch(axis.line, 1)); - const expected: Position = patch(axis.line, config.maxScrollSpeed); + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(target)); - requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); - expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); - it('should not scroll if the item is too big', () => { - const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBig.descriptor, - }, + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, }, - }; + }, + }; - return addDraggable(updated, tooBig); - })(); + return addDraggable(updated, tooBig); + })(); - autoScroller.onStateChange(state.idle, custom); + autoScroller.onStateChange(state.idle, custom); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - it('should not scroll if the window cannot scroll', () => { - setWindowScrollSize({ - scrollHeight: viewport.height, - scrollWidth: viewport.width, - }); - const target: Position = onMaxBoundary; + it('should not scroll if the window cannot scroll', () => { + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, + }); + const target: Position = onMaxBoundary; - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange(state.idle, dragTo(target)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); + }); - describe('moving backwards towards the start of window', () => { - const windowScroll: Position = patch(axis.line, 10); - - beforeEach(() => { - setWindowScroll(windowScroll); - }); + describe('moving backwards towards the start of window', () => { + const windowScroll: Position = patch(axis.line, 10); - const onStartBoundary: Position = patch( - axis.line, - // at the boundary is not enough to start - windowScroll[axis.line] + thresholds.startFrom, - viewport.center[axis.crossAxisLine], - ); - const onMaxBoundary: Position = patch( - axis.line, - (windowScroll[axis.line] + thresholds.maxSpeedAt), - viewport.center[axis.crossAxisLine], - ); + beforeEach(() => { + setWindowScroll(windowScroll); + }); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + const onStartBoundary: Position = patch( + axis.line, + // at the boundary is not enough to start + windowScroll[axis.line] + thresholds.startFrom, + viewport.center[axis.crossAxisLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (windowScroll[axis.line] + thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); + autoScroller.onStateChange(state.idle, dragTo(target)); - autoScroller.onStateChange(state.idle, dragTo(target)); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving backwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.line]).toBeLessThan(0); + }); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); - // moving backwards - const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.line]).toBeLessThan(0); - }); + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); - it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); - const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange(state.idle, dragTo(target1)); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); - autoScroller.onStateChange(state.idle, dragTo(target1)); - autoScroller.onStateChange(dragTo(target1), dragTo(target2)); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - // verification - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + // not testing value called as we are not exposing getRequired scroll + }); - // not testing value called as we are not exposing getRequired scroll - }); + it('should get faster the closer to the max speed point', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); - it('should get faster the closer to the max speed point', () => { - const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); - const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange(state.idle, dragTo(target1)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); - autoScroller.onStateChange(state.idle, dragTo(target1)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(1); - const scroll1: Position = (mocks.scrollWindow.mock.calls[0][0] : any); + autoScroller.onStateChange(dragTo(target1), dragTo(target2)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); - autoScroller.onStateChange(dragTo(target1), dragTo(target2)); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalledTimes(2); - const scroll2: Position = (mocks.scrollWindow.mock.calls[1][0] : any); + // moving backwards so a smaller value is bigger + expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); + // or put another way: + expect(Math.abs(scroll1[axis.line])).toBeLessThan(Math.abs(scroll2[axis.line])); - // moving backwards so a smaller value is bigger - expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); - // or put another way: - expect(Math.abs(scroll1[axis.line])).toBeLessThan(Math.abs(scroll2[axis.line])); + // validation + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); + }); - // validation - expect(scroll1[axis.crossAxisLine]).toBe(0); - expect(scroll2[axis.crossAxisLine]).toBe(0); - }); + it('should have the top speed at the max speed point', () => { + const target: Position = onMaxBoundary; + const expected: Position = patch(axis.line, -config.maxScrollSpeed); - it('should have the top speed at the max speed point', () => { - const target: Position = onMaxBoundary; - const expected: Position = patch(axis.line, -config.maxScrollSpeed); + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(target)); - requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); - expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); - }); + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, -config.maxScrollSpeed); - it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); - const expected: Position = patch(axis.line, -config.maxScrollSpeed); + autoScroller.onStateChange(state.idle, dragTo(target)); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(target)); - requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + }); - expect(mocks.scrollWindow).toHaveBeenCalledWith(expected); + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); - it('should not scroll if the item is too big', () => { - const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBig.descriptor, - }, + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, }, - }; - - return addDraggable(updated, tooBig); - })(); - - autoScroller.onStateChange(state.idle, custom); - - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + }, + }; - it('should not scroll if the window cannot scroll', () => { - setWindowScrollSize({ - scrollHeight: viewport.height, - scrollWidth: viewport.width, - }); - const target: Position = onMaxBoundary; + return addDraggable(updated, tooBig); + })(); - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange(state.idle, custom); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); - // just some light tests to ensure that cross axis moving also works - describe('moving forward on the cross axis', () => { - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - viewport, - axis === vertical ? horizontal : vertical, - ); + it('should not scroll if the window cannot scroll', () => { + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, + }); + const target: Position = onMaxBoundary; - const onStartBoundary: Position = patch( - axis.line, - viewport.center[axis.line], - // to the boundary is not enough to start - (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom), - ); + autoScroller.onStateChange(state.idle, dragTo(target)); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + requestAnimationFrame.step(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + // just some light tests to ensure that cross axis moving also works + describe('moving forward on the cross axis', () => { + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + viewport, + axis === vertical ? horizontal : vertical, + ); + + const onStartBoundary: Position = patch( + axis.line, + viewport.center[axis.line], + // to the boundary is not enough to start + (viewport[axis.crossAxisSize] - crossAxisThresholds.startFrom), + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange(state.idle, dragTo(target)); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); - // moving forwards - const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.crossAxisLine]).toBeGreaterThan(0); - }); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving forwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.crossAxisLine]).toBeGreaterThan(0); }); + }); - // just some light tests to ensure that cross axis moving also works - describe('moving backward on the cross axis', () => { - const windowScroll: Position = patch(axis.crossAxisLine, 10); - beforeEach(() => { - setWindowScroll(windowScroll); - }); + // just some light tests to ensure that cross axis moving also works + describe('moving backward on the cross axis', () => { + const windowScroll: Position = patch(axis.crossAxisLine, 10); + beforeEach(() => { + setWindowScroll(windowScroll); + }); - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - viewport, - axis === vertical ? horizontal : vertical, - ); + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + viewport, + axis === vertical ? horizontal : vertical, + ); - const onStartBoundary: Position = patch( - axis.line, - viewport.center[axis.line], - // to the boundary is not enough to start - windowScroll[axis.crossAxisLine] + (crossAxisThresholds.startFrom) - ); + const onStartBoundary: Position = patch( + axis.line, + viewport.center[axis.line], + // to the boundary is not enough to start + windowScroll[axis.crossAxisLine] + (crossAxisThresholds.startFrom) + ); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); - requestAnimationFrame.flush(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); - autoScroller.onStateChange(state.idle, dragTo(target)); + autoScroller.onStateChange(state.idle, dragTo(target)); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); - // moving backwards - const request: Position = mocks.scrollWindow.mock.calls[0][0]; - expect(request[axis.crossAxisLine]).toBeLessThan(0); - }); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalled(); + // moving backwards + const request: Position = mocks.scrollWindow.mock.calls[0][0]; + expect(request[axis.crossAxisLine]).toBeLessThan(0); }); }); + }); - describe('droppable scrolling', () => { - const thresholds: PixelThresholds = getPixelThresholds(frame, axis); + describe('droppable scrolling', () => { + const thresholds: PixelThresholds = getPixelThresholds(frame, axis); - beforeEach(() => { - // avoiding any window scrolling - setWindowScrollSize({ - scrollHeight: viewport.height, - scrollWidth: viewport.width, - }); + beforeEach(() => { + // avoiding any window scrolling + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, }); + }); - describe('moving forward to end of droppable', () => { - const onStartBoundary: Position = patch( - axis.line, - // to the boundary is not enough to start - (frame[axis.size] - thresholds.startFrom), - frame.center[axis.crossAxisLine], - ); - const onMaxBoundary: Position = patch( - axis.line, - (frame[axis.size] - thresholds.maxSpeedAt), - frame.center[axis.crossAxisLine], + describe('moving forward to end of droppable', () => { + const onStartBoundary: Position = patch( + axis.line, + // to the boundary is not enough to start + (frame[axis.size] - thresholds.startFrom), + frame.center[axis.crossAxisLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (frame[axis.size] - thresholds.maxSpeedAt), + frame.center[axis.crossAxisLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onStartBoundary), scrollable) ); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onStartBoundary), scrollable) - ); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.line, 1)); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.line, 1)); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrollable), + ); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrollable), - ); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + // moving forwards + const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalled(); - // moving forwards - const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + expect(id).toBe(scrollable.descriptor.id); + expect(scroll[axis.line]).toBeGreaterThan(0); + expect(scroll[axis.crossAxisLine]).toBe(0); + }); - expect(id).toBe(scrollable.descriptor.id); - expect(scroll[axis.line]).toBeGreaterThan(0); - expect(scroll[axis.crossAxisLine]).toBe(0); - }); + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); - it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = add(onStartBoundary, patch(axis.line, 1)); - const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrollable), + ); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrollable), + addDroppable(dragTo(target2), scrollable), + ); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target1), scrollable), - ); - autoScroller.onStateChange( - addDroppable(dragTo(target1), scrollable), - addDroppable(dragTo(target2), scrollable), - ); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - // verification - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + // not testing value called as we are not exposing getRequired scroll + }); - // not testing value called as we are not exposing getRequired scroll - }); + it('should get faster the closer to the max speed point', () => { + const target1: Position = add(onStartBoundary, patch(axis.line, 1)); + const target2: Position = add(onStartBoundary, patch(axis.line, 2)); - it('should get faster the closer to the max speed point', () => { - const target1: Position = add(onStartBoundary, patch(axis.line, 1)); - const target2: Position = add(onStartBoundary, patch(axis.line, 2)); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrollable), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target1), scrollable), - ); - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrollable), + addDroppable(dragTo(target2), scrollable), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); - autoScroller.onStateChange( - addDroppable(dragTo(target1), scrollable), - addDroppable(dragTo(target2), scrollable), - ); - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); - const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); + expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); - expect(scroll1[axis.line]).toBeLessThan(scroll2[axis.line]); + // validation + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); + }); - // validation - expect(scroll1[axis.crossAxisLine]).toBe(0); - expect(scroll2[axis.crossAxisLine]).toBe(0); - }); + it('should have the top speed at the max speed point', () => { + const expected: Position = patch(axis.line, config.maxScrollSpeed); - it('should have the top speed at the max speed point', () => { - const expected: Position = patch(axis.line, config.maxScrollSpeed); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onMaxBoundary), scrollable), + ); + requestAnimationFrame.step(); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onMaxBoundary), scrollable), - ); - requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - expected - ); - }); + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = add(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, config.maxScrollSpeed); - it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = add(onMaxBoundary, patch(axis.line, 1)); - const expected: Position = patch(axis.line, config.maxScrollSpeed); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrollable), + ); + requestAnimationFrame.step(); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrollable), - ); - requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - expected - ); + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); - it('should not scroll if the item is too big', () => { - const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBig.descriptor, - }, + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, }, - }; - - return addDroppable(addDraggable(updated, tooBig), scrollable); - })(); - - autoScroller.onStateChange(state.idle, custom); + }, + }; - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + return addDroppable(addDraggable(updated, tooBig), scrollable); + })(); - it('should not scroll if the droppable is unable to be scrolled', () => { - const target: Position = onMaxBoundary; - if (!scrollable.viewport.closestScrollable) { - throw new Error('Invalid test setup'); - } - // scrolling to max scroll point - const maxChange: Position = scrollable.viewport.closestScrollable.scroll.max; - const scrolled: DroppableDimension = scrollDroppable(scrollable, maxChange); - - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled) - ); - requestAnimationFrame.flush(); + autoScroller.onStateChange(state.idle, custom); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); - describe('moving backward to the start of droppable', () => { - const droppableScroll: Position = patch(axis.line, 10); - const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); + it('should not scroll if the droppable is unable to be scrolled', () => { + const target: Position = onMaxBoundary; + if (!scrollable.viewport.closestScrollable) { + throw new Error('Invalid test setup'); + } + // scrolling to max scroll point + const maxChange: Position = scrollable.viewport.closestScrollable.scroll.max; + const scrolled: DroppableDimension = scrollDroppable(scrollable, maxChange); - const onStartBoundary: Position = patch( - axis.line, - // to the boundary is not enough to start - (frame[axis.start] + thresholds.startFrom), - frame.center[axis.crossAxisLine], - ); - const onMaxBoundary: Position = patch( - axis.line, - (frame[axis.start] + thresholds.maxSpeedAt), - frame.center[axis.crossAxisLine], + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled) ); + requestAnimationFrame.flush(); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onStartBoundary), scrolled) - ); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + describe('moving backward to the start of droppable', () => { + const droppableScroll: Position = patch(axis.line, 10); + const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); + + const onStartBoundary: Position = patch( + axis.line, + // to the boundary is not enough to start + (frame[axis.start] + thresholds.startFrom), + frame.center[axis.crossAxisLine], + ); + const onMaxBoundary: Position = patch( + axis.line, + (frame[axis.start] + thresholds.maxSpeedAt), + frame.center[axis.crossAxisLine], + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onStartBoundary), scrolled) + ); - it('should scroll if moving beyond the start threshold', () => { - // going backwards - const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled), - ); + it('should scroll if moving beyond the start threshold', () => { + // going backwards + const target: Position = subtract(onStartBoundary, patch(axis.line, 1)); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), + ); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalled(); - const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - // validation - expect(id).toBe(scrollable.descriptor.id); - // moving backwards - expect(scroll[axis.line]).toBeLessThan(0); - expect(scroll[axis.crossAxisLine]).toBe(0); - }); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + + // validation + expect(id).toBe(scrollable.descriptor.id); + // moving backwards + expect(scroll[axis.line]).toBeLessThan(0); + expect(scroll[axis.crossAxisLine]).toBe(0); + }); - it('should throttle multiple scrolls into a single animation frame', () => { - const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); - const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + it('should throttle multiple scrolls into a single animation frame', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target1), scrolled), - ); - autoScroller.onStateChange( - addDroppable(dragTo(target1), scrolled), - addDroppable(dragTo(target2), scrolled), - ); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrolled), + ); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrolled), + addDroppable(dragTo(target2), scrolled), + ); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - // verification - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + // verification + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - // not testing value called as we are not exposing getRequired scroll - }); + // not testing value called as we are not exposing getRequired scroll + }); - it('should get faster the closer to the max speed point', () => { - const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); - const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); + it('should get faster the closer to the max speed point', () => { + const target1: Position = subtract(onStartBoundary, patch(axis.line, 1)); + const target2: Position = subtract(onStartBoundary, patch(axis.line, 2)); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target1), scrolled), - ); - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); - const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target1), scrolled), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(1); + const scroll1: Position = (mocks.scrollDroppable.mock.calls[0][1] : any); - autoScroller.onStateChange( - addDroppable(dragTo(target1), scrolled), - addDroppable(dragTo(target2), scrolled), - ); - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); - const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); + autoScroller.onStateChange( + addDroppable(dragTo(target1), scrolled), + addDroppable(dragTo(target2), scrolled), + ); + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalledTimes(2); + const scroll2: Position = (mocks.scrollDroppable.mock.calls[1][1] : any); - // moving backwards - expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); + // moving backwards + expect(scroll1[axis.line]).toBeGreaterThan(scroll2[axis.line]); - // validation - expect(scroll1[axis.crossAxisLine]).toBe(0); - expect(scroll2[axis.crossAxisLine]).toBe(0); - }); + // validation + expect(scroll1[axis.crossAxisLine]).toBe(0); + expect(scroll2[axis.crossAxisLine]).toBe(0); + }); - it('should have the top speed at the max speed point', () => { - const expected: Position = patch(axis.line, -config.maxScrollSpeed); + it('should have the top speed at the max speed point', () => { + const expected: Position = patch(axis.line, -config.maxScrollSpeed); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onMaxBoundary), scrolled), - ); - requestAnimationFrame.step(); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onMaxBoundary), scrolled), + ); + requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - expected - ); - }); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); - it('should have the top speed when moving beyond the max speed point', () => { - const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); - const expected: Position = patch(axis.line, -config.maxScrollSpeed); + it('should have the top speed when moving beyond the max speed point', () => { + const target: Position = subtract(onMaxBoundary, patch(axis.line, 1)); + const expected: Position = patch(axis.line, -config.maxScrollSpeed); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled), - ); - requestAnimationFrame.step(); + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), + ); + requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - expected - ); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); + + it('should not scroll if the item is too big', () => { + const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); - it('should not scroll if the item is too big', () => { - const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBig.descriptor, - }, + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, }, - }; - - return addDroppable(addDraggable(updated, tooBig), scrolled); - })(); + }, + }; - autoScroller.onStateChange(state.idle, custom); + return addDroppable(addDraggable(updated, tooBig), scrolled); + })(); - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); - - it('should not scroll if the droppable is unable to be scrolled', () => { - const target: Position = onMaxBoundary; - if (!scrollable.viewport.closestScrollable) { - throw new Error('Invalid test setup'); - } - // scrolling to max scroll point - - autoScroller.onStateChange( - state.idle, - // scrollable cannot be scrolled backwards - addDroppable(dragTo(target), scrollable) - ); - requestAnimationFrame.flush(); + autoScroller.onStateChange(state.idle, custom); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); - // just some light tests to ensure that cross axis moving also works - describe('moving forward on the cross axis', () => { - const droppableScroll: Position = patch(axis.crossAxisLine, 10); - const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); + it('should not scroll if the droppable is unable to be scrolled', () => { + const target: Position = onMaxBoundary; + if (!scrollable.viewport.closestScrollable) { + throw new Error('Invalid test setup'); + } + // scrolling to max scroll point - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - frame, - axis === vertical ? horizontal : vertical, - ); - - const onStartBoundary: Position = patch( - axis.line, - frame.center[axis.line], - // to the boundary is not enough to start - (frame[axis.crossAxisSize] - crossAxisThresholds.startFrom), + autoScroller.onStateChange( + state.idle, + // scrollable cannot be scrolled backwards + addDroppable(dragTo(target), scrollable) ); + requestAnimationFrame.flush(); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); - - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); - it('should scroll if moving beyond the start threshold', () => { - const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); + // just some light tests to ensure that cross axis moving also works + describe('moving forward on the cross axis', () => { + const droppableScroll: Position = patch(axis.crossAxisLine, 10); + const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled), - ); + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + frame, + axis === vertical ? horizontal : vertical, + ); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + const onStartBoundary: Position = patch( + axis.line, + frame.center[axis.line], + // to the boundary is not enough to start + (frame[axis.crossAxisSize] - crossAxisThresholds.startFrom), + ); - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalled(); - // moving forwards - const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange(state.idle, dragTo(onStartBoundary)); - expect(id).toBe(scrolled.descriptor.id); - expect(scroll[axis.crossAxisLine]).toBeGreaterThan(0); - }); + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); - // just some light tests to ensure that cross axis moving also works - describe('moving backward on the cross axis', () => { - const droppableScroll: Position = patch(axis.crossAxisLine, 10); - const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - frame, - axis === vertical ? horizontal : vertical, - ); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = add(onStartBoundary, patch(axis.crossAxisLine, 1)); - const onStartBoundary: Position = patch( - axis.line, - frame.center[axis.line], - // to the boundary is not enough to start - (frame[axis.crossAxisStart] + crossAxisThresholds.startFrom) + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), ); - it('should not scroll if not past the start threshold', () => { - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(onStartBoundary), scrolled), - ); - - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); - - it('should scroll if moving beyond the start threshold', () => { - const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); - - autoScroller.onStateChange( - state.idle, - addDroppable(dragTo(target), scrolled), - ); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + // moving forwards + const [id, scroll] = mocks.scrollDroppable.mock.calls[0]; - // only called after a frame - requestAnimationFrame.step(); - expect(mocks.scrollDroppable).toHaveBeenCalled(); - // moving backwards - const request: Position = mocks.scrollDroppable.mock.calls[0][1]; - expect(request[axis.crossAxisLine]).toBeLessThan(0); - }); + expect(id).toBe(scrolled.descriptor.id); + expect(scroll[axis.crossAxisLine]).toBeGreaterThan(0); }); }); - describe('window scrolling before droppable scrolling', () => { - const custom: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'scrollable that is similiar to the viewport', - type: 'TYPE', - }, - client: getArea({ - top: 0, - left: 0, - // bigger than the frame - right: windowScrollSize.scrollWidth, - bottom: windowScrollSize.scrollHeight, - }), - closest: { - frameClient: viewport, - scrollWidth: windowScrollSize.scrollWidth, - scrollHeight: windowScrollSize.scrollHeight, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, + // just some light tests to ensure that cross axis moving also works + describe('moving backward on the cross axis', () => { + const droppableScroll: Position = patch(axis.crossAxisLine, 10); + const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + frame, + axis === vertical ? horizontal : vertical, + ); + + const onStartBoundary: Position = patch( + axis.line, + frame.center[axis.line], + // to the boundary is not enough to start + (frame[axis.crossAxisStart] + crossAxisThresholds.startFrom) + ); + + it('should not scroll if not past the start threshold', () => { + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(onStartBoundary), scrolled), + ); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); - const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); - it('should scroll the window only if both the window and droppable can be scrolled', () => { - const onMaxBoundary: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.maxSpeedAt), - viewport.center[axis.crossAxisLine], - ); + it('should scroll if moving beyond the start threshold', () => { + const target: Position = subtract(onStartBoundary, patch(axis.crossAxisLine, 1)); autoScroller.onStateChange( state.idle, - addDroppable(dragTo(onMaxBoundary), custom), + addDroppable(dragTo(target), scrolled), ); - requestAnimationFrame.step(); - expect(mocks.scrollWindow).toHaveBeenCalled(); expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + + // only called after a frame + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).toHaveBeenCalled(); + // moving backwards + const request: Position = mocks.scrollDroppable.mock.calls[0][1]; + expect(request[axis.crossAxisLine]).toBeLessThan(0); }); }); + }); + + describe('window scrolling before droppable scrolling', () => { + const custom: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'scrollable that is similiar to the viewport', + type: 'TYPE', + }, + client: getArea({ + top: 0, + left: 0, + // bigger than the frame + right: windowScrollSize.scrollWidth, + bottom: windowScrollSize.scrollHeight, + }), + closest: { + frameClient: viewport, + scrollWidth: windowScrollSize.scrollWidth, + scrollHeight: windowScrollSize.scrollHeight, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); + + it('should scroll the window only if both the window and droppable can be scrolled', () => { + const onMaxBoundary: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); - describe('on drag end', () => { - const endDragStates = [ + autoScroller.onStateChange( state.idle, - state.dropAnimating(), - state.userCancel(), - state.dropComplete(), - ]; - - endDragStates.forEach((end: State) => { - it('should cancel any pending window scroll', () => { - const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); - const onMaxBoundary: Position = patch( - axis.line, - (viewport[axis.size] - thresholds.maxSpeedAt), - viewport.center[axis.crossAxisLine], - ); + addDroppable(dragTo(onMaxBoundary), custom), + ); + requestAnimationFrame.step(); - autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); + expect(mocks.scrollWindow).toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); - // frame not cleared - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + describe('on drag end', () => { + const endDragStates = [ + state.idle, + state.dropAnimating(), + state.userCancel(), + state.dropComplete(), + ]; + + endDragStates.forEach((end: State) => { + it('should cancel any pending window scroll', () => { + const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); + const onMaxBoundary: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + viewport.center[axis.crossAxisLine], + ); - // should cancel the next frame - autoScroller.onStateChange(dragTo(onMaxBoundary), end); - requestAnimationFrame.flush(); + autoScroller.onStateChange(state.idle, dragTo(onMaxBoundary)); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - }); + // frame not cleared + expect(mocks.scrollWindow).not.toHaveBeenCalled(); - it('should cancel any pending droppable scroll', () => { - const thresholds: PixelThresholds = getPixelThresholds(frame, axis); - const onMaxBoundary: Position = patch( - axis.line, - (frame[axis.size] - thresholds.maxSpeedAt), - frame.center[axis.crossAxisLine], - ); - const drag: State = addDroppable(dragTo(onMaxBoundary), scrollable); + // should cancel the next frame + autoScroller.onStateChange(dragTo(onMaxBoundary), end); + requestAnimationFrame.flush(); - autoScroller.onStateChange( - state.idle, - drag - ); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + + it('should cancel any pending droppable scroll', () => { + const thresholds: PixelThresholds = getPixelThresholds(frame, axis); + const onMaxBoundary: Position = patch( + axis.line, + (frame[axis.size] - thresholds.maxSpeedAt), + frame.center[axis.crossAxisLine], + ); + const drag: State = addDroppable(dragTo(onMaxBoundary), scrollable); + + autoScroller.onStateChange( + state.idle, + drag + ); - // frame not cleared - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + // frame not cleared + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - // should cancel the next frame - autoScroller.onStateChange(drag, end); - requestAnimationFrame.flush(); + // should cancel the next frame + autoScroller.onStateChange(drag, end); + requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); }); }); diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index 2aa70a0bb8..c76797de93 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -22,13 +22,20 @@ import { getPreset } from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; +const origin: Position = { x: 0, y: 0 }; + const windowScrollSize = { scrollHeight: 2000, scrollWidth: 1600, }; +const viewport: Area = getArea({ + top: 0, + left: 0, + right: 800, + bottom: 1000, +}); - -describe('jump scroller', () => { +describe('jump auto scrolling', () => { let autoScroller: AutoScroller; let mocks; @@ -39,6 +46,8 @@ describe('jump scroller', () => { move: jest.fn(), }; autoScroller = createAutoScroller(mocks); + setViewport(viewport); + setWindowScrollSize(windowScrollSize); }); afterEach(() => { @@ -48,21 +57,85 @@ describe('jump scroller', () => { requestAnimationFrame.reset(); }); - describe('window scrolling', () => { - it('should not scroll if the item is bigger than the viewport', () => { + [vertical, horizontal].forEach((axis: Axis) => { + describe(`on the ${axis.direction} axis`, () => { + const preset = getPreset(axis); - }); + describe('window scrolling', () => { + it('should not scroll if the item is bigger than the viewport', () => { - it('should manually move the item if the window is unable to scroll', () => { + }); - }); + describe('moving forwards', () => { + it('should manually move the item if the window is unable to scroll', () => { + // disabling scroll + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, + }); + const request: Position = patch(axis.line, 1); + const current: State = state.scrollJumpRequest(request); + if (!current.drag) { + throw new Error('invalid state'); + } + const expected: Position = add(current.drag.current.client.selection, request); - it('should scroll the window if can absorb all of the movement', () => { + autoScroller.onStateChange(state.idle, current); - }); + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expected, + origin, + true, + ); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + + it('should scroll the window if can absorb all of the movement', () => { + const request: Position = patch(axis.line, 1); + + autoScroller.onStateChange(state.idle, state.scrollJumpRequest(request)); + + expect(mocks.scrollWindow).toHaveBeenCalledWith(request); + expect(mocks.move).not.toHaveBeenCalled(); + }); + + it('should manually move the item any distance that the window is unable to scroll', () => { + // only allowing scrolling by 1 px + setWindowScrollSize({ + scrollHeight: viewport.height + 1, + scrollWidth: viewport.width + 1, + }); + // more than the 1 pixel allowed + const request: Position = patch(axis.line, 3); + const current: State = state.scrollJumpRequest(request); + if (!current.drag) { + throw new Error('invalid state'); + } + const expected: Position = add( + current.drag.current.client.selection, + // the two pixels that could not be done by the window + patch(axis.line, 2) + ); + + autoScroller.onStateChange(state.idle, state.scrollJumpRequest(request)); + + // can scroll with what we have + expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, 1)); + // remainder to be done by movement + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expected, + origin, + true, + ); + }); + }); - it('should manually move the item any distance that the window is unable to scroll', () => { + describe('moving backwards', () => { + }); + }); }); }); -}); \ No newline at end of file +}); diff --git a/test/utils/simple-state-preset.js b/test/utils/simple-state-preset.js index 8da8027186..bbfb0f8443 100644 --- a/test/utils/simple-state-preset.js +++ b/test/utils/simple-state-preset.js @@ -16,6 +16,7 @@ import type { PendingDrop, DropTrigger, DraggableId, + DragImpact, } from '../../src/types'; const preset = getPreset(); @@ -110,6 +111,62 @@ export const dragging = ( return result; }; +export const scrollJumpRequest = (request: Position): State => { + const id: DraggableId = preset.inHome1.descriptor.id; + // will populate the dimension state with the initial dimensions + const draggable: DraggableDimension = preset.draggables[id]; + // either use the provided selection or use the draggable's center + const initialPosition: InitialDragPositions = { + selection: draggable.client.withMargin.center, + center: draggable.client.withMargin.center, + }; + const clientPositions: CurrentDragPositions = { + selection: draggable.client.withMargin.center, + center: draggable.client.withMargin.center, + offset: origin, + }; + + const impact: DragImpact = { + movement: { + displaced: [], + amount: origin, + isBeyondStartPosition: false, + }, + direction: preset.home.axis.direction, + destination: { + index: preset.inHome1.descriptor.index, + droppableId: preset.inHome1.descriptor.droppableId, + }, + }; + + const drag: DragState = { + initial: { + descriptor: draggable.descriptor, + autoScrollMode: 'JUMP', + client: initialPosition, + page: initialPosition, + windowScroll: origin, + }, + current: { + client: clientPositions, + page: clientPositions, + windowScroll: origin, + shouldAnimate: true, + }, + impact, + scrollJumpRequest: request, + }; + + const result: State = { + phase: 'DRAGGING', + drag, + drop: null, + dimension: getDimensionState(id), + }; + + return result; +} + const getDropAnimating = (id: DraggableId, trigger: DropTrigger): State => { const descriptor: DraggableDescriptor = preset.draggables[id].descriptor; const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor; From 61d4eb82e55260877abbf70c6590815c558814b8 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 11:25:06 +1100 Subject: [PATCH 069/163] finalising tests for jump window scrolling --- src/state/auto-scroll/create-jump-scroller.js | 3 +- .../state/auto-scroll/jump-scroller.spec.js | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/state/auto-scroll/create-jump-scroller.js b/src/state/auto-scroll/create-jump-scroller.js index 96f98ffcb8..219ae7b59b 100644 --- a/src/state/auto-scroll/create-jump-scroller.js +++ b/src/state/auto-scroll/create-jump-scroller.js @@ -42,11 +42,10 @@ export default ({ const moveByOffset = (state: State, offset: Position) => { const drag: ?DragState = state.drag; if (!drag) { + console.error('Cannot move by offset when not dragging'); return; } - console.warn('moving by offset', offset); - const client: Position = add(drag.current.client.selection, offset); move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); }; diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index c76797de93..25667a785e 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -133,7 +133,64 @@ describe('jump auto scrolling', () => { }); describe('moving backwards', () => { + it('should manually move the item if the window is unable to scroll', () => { + // unable to scroll backwards to start with + const request: Position = patch(axis.line, -1); + const current: State = state.scrollJumpRequest(request); + if (!current.drag) { + throw new Error('invalid state'); + } + const expected: Position = add(current.drag.current.client.selection, request); + + autoScroller.onStateChange(state.idle, current); + + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expected, + origin, + true, + ); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + + it('should scroll the window if can absorb all of the movement', () => { + setWindowScroll(patch(axis.line, 1)); + const request: Position = patch(axis.line, -1); + + autoScroller.onStateChange(state.idle, state.scrollJumpRequest(request)); + expect(mocks.scrollWindow).toHaveBeenCalledWith(request); + expect(mocks.move).not.toHaveBeenCalled(); + }); + + it('should manually move the item any distance that the window is unable to scroll', () => { + // only allowing scrolling by 1 px + const windowScroll: Position = patch(axis.line, 1); + setWindowScroll(windowScroll); + // more than the 1 pixel allowed + const request: Position = patch(axis.line, -3); + const current: State = state.scrollJumpRequest(request); + if (!current.drag) { + throw new Error('invalid state'); + } + const expected: Position = add( + current.drag.current.client.selection, + // the two pixels that could not be done by the window + patch(axis.line, -2) + ); + + autoScroller.onStateChange(state.idle, current); + + // can scroll with what we have + expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, -1)); + // remainder to be done by movement + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expected, + windowScroll, + true, + ); + }); }); }); }); From 275e9036903536479de8799f59206aa86aaa880e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 11:42:20 +1100 Subject: [PATCH 070/163] skelton for jump droppable tests --- src/state/auto-scroll/create-jump-scroller.js | 21 ++++++++--- .../state/auto-scroll/jump-scroller.spec.js | 36 +++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/state/auto-scroll/create-jump-scroller.js b/src/state/auto-scroll/create-jump-scroller.js index 219ae7b59b..2a1db42c49 100644 --- a/src/state/auto-scroll/create-jump-scroller.js +++ b/src/state/auto-scroll/create-jump-scroller.js @@ -97,7 +97,15 @@ export default ({ scrollDroppable(droppable.descriptor.id, request); return; } - // there is overlap - can the window absorb it? + + // Droppable cannot absorb the entire request + + // Let the droppable scroll what it can + const whatTheDroppableCanScroll: Position = subtract(request, overlap); + // TODO: is it okay that this is before a move? + scrollDroppable(droppable.descriptor.id, whatTheDroppableCanScroll); + + // Okay, now we need to find out where the rest of the movement can come from. const canWindowScrollOverlap: boolean = canScrollWindow(overlap); @@ -107,7 +115,7 @@ export default ({ return; } - // how much can the window aborb? + // how much can the window absorb? const windowOverlap: ?Position = getWindowOverlap(overlap); // window can absorb all of the overlap @@ -119,7 +127,10 @@ export default ({ // window can only partially absorb overlap // need to move the item by the remainder and scroll the window moveByOffset(state, windowOverlap); - scrollWindow(overlap); + + // the amount that the window can actually scroll + const whatTheWindowCanScroll: Position = subtract(overlap, windowOverlap); + scrollWindow(whatTheWindowCanScroll); return; } @@ -141,8 +152,8 @@ export default ({ moveByOffset(state, overlap); // the amount that the window can actually scroll - const canScroll: Position = subtract(request, overlap); - scrollWindow(canScroll); + const whatTheWindowCanScroll: Position = subtract(request, overlap); + scrollWindow(whatTheWindowCanScroll); }; return jumpScroller; diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index 25667a785e..c84c7e0777 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -193,6 +193,42 @@ describe('jump auto scrolling', () => { }); }); }); + + describe('droppable scrolling (which can involve some window scrolling)', () => { + it('should not scroll if the item is bigger than the viewport', () => { + + }); + + describe('moving forwards', () => { + it('should scroll droppable the entire request if it is able to', () => { + + }); + + describe('draggable is unable to complete the entire scroll', () => { + it('should manually move the entire request if it is unable to be completed by the window or the droppabe', () => { + + }); + + describe('window cannot absorb any of the scroll', () => { + it('should move the remainder', () => { + + }); + }); + + describe('window can absorb some of the scroll', () => { + it('should do the entire move if it can', () => { + + }); + + it('should scroll the window by what it can, and move the rest', () => { + + }); + }); + }); + + + }); + }); }); }); }); From fa1ce2d5de05a02ef1aea9d823f19998a451eb12 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 12:46:26 +1100 Subject: [PATCH 071/163] jump scroller tests --- .../state/auto-scroll/jump-scroller.spec.js | 216 ++++++++++++++++-- 1 file changed, 201 insertions(+), 15 deletions(-) diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index c84c7e0777..0c2202759e 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -21,6 +21,7 @@ import * as state from '../../../utils/simple-state-preset'; import { getPreset } from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; +import getMaxScroll from '../../../../src/state/get-max-scroll'; const origin: Position = { x: 0, y: 0 }; @@ -35,6 +36,24 @@ const viewport: Area = getArea({ bottom: 1000, }); +const addDroppable = (base: State, droppable: DroppableDimension): State => ({ + ...base, + dimension: { + ...base.dimension, + droppable: { + ...base.dimension.droppable, + [droppable.descriptor.id]: droppable, + }, + }, +}); + +const disableWindowScroll = () => { + setWindowScrollSize({ + scrollHeight: viewport.height, + scrollWidth: viewport.width, + }) +}; + describe('jump auto scrolling', () => { let autoScroller: AutoScroller; let mocks; @@ -68,11 +87,7 @@ describe('jump auto scrolling', () => { describe('moving forwards', () => { it('should manually move the item if the window is unable to scroll', () => { - // disabling scroll - setWindowScrollSize({ - scrollHeight: viewport.height, - scrollWidth: viewport.width, - }); + disableWindowScroll(); const request: Position = patch(axis.line, 1); const current: State = state.scrollJumpRequest(request); if (!current.drag) { @@ -195,33 +210,204 @@ describe('jump auto scrolling', () => { }); describe('droppable scrolling (which can involve some window scrolling)', () => { + const scrollableScrollSize = { + scrollWidth: 800, + scrollHeight: 800, + }; + const frame: Area = getArea({ + top: 0, + left: 0, + right: 600, + bottom: 600, + }); + const scrollable: DroppableDimension = getDroppableDimension({ + // stealing the home descriptor so that the dragging item will + // be within in + descriptor: preset.home.descriptor, + client: getArea({ + top: 0, + left: 0, + // bigger than the frame + right: scrollableScrollSize.scrollWidth, + bottom: scrollableScrollSize.scrollHeight, + }), + closest: { + frameClient: frame, + scrollWidth: scrollableScrollSize.scrollWidth, + scrollHeight: scrollableScrollSize.scrollHeight, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + if (!scrollable.viewport.closestScrollable) { + throw new Error('Invalid droppable'); + } + + const maxDroppableScroll: Position = + scrollable.viewport.closestScrollable.scroll.max; + it('should not scroll if the item is bigger than the viewport', () => { }); describe('moving forwards', () => { - it('should scroll droppable the entire request if it is able to', () => { - + describe('droppable is able to complete entire scroll', () => { + it('should only scroll the droppable', () => { + const request: Position = patch(axis.line, 1); + + autoScroller.onStateChange( + state.idle, + addDroppable(state.scrollJumpRequest(request), scrollable), + ); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + request, + ); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.move).not.toHaveBeenCalled(); + }); }); - describe('draggable is unable to complete the entire scroll', () => { - it('should manually move the entire request if it is unable to be completed by the window or the droppabe', () => { - + describe('droppable is unable to complete the entire scroll', () => { + it('should manually move the entire request if it is unable to be partially completed by the window or the droppable', () => { + // droppable can no longer be scrolled + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + maxDroppableScroll, + ); + disableWindowScroll(); + const request: Position = patch(axis.line, 1); + const current: State = state.scrollJumpRequest(request); + if (!current.drag) { + throw new Error('invalid state'); + } + const expected: Position = add(current.drag.current.client.selection, request); + + autoScroller.onStateChange( + state.idle, + addDroppable(current, scrolled), + ); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expected, + origin, + true, + ); }); - describe('window cannot absorb any of the scroll', () => { - it('should move the remainder', () => { + describe('window is unable to absorb some of the scroll', () => { + beforeEach(() => { + disableWindowScroll(); + }); + it('should scroll the droppable what it can and move the rest', () => { + // able to scroll 1 pixel forward + const availableScroll: Position = patch(axis.line, 1); + const scroll: Position = subtract(maxDroppableScroll, availableScroll); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + scroll, + ); + // want to move 3 pixels + const request: Position = patch(axis.line, 3); + const current: State = state.scrollJumpRequest(request); + if (!current.drag) { + throw new Error('invalid state'); + } + const expectedManualMove: Position = + add(current.drag.current.client.selection, patch(axis.line, 2)); + + autoScroller.onStateChange( + state.idle, + addDroppable(current, scrolled), + ); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + preset.home.descriptor.id, + availableScroll, + ); + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expectedManualMove, + origin, + true, + ); }); }); describe('window can absorb some of the scroll', () => { - it('should do the entire move if it can', () => { - + it('should scroll the entire overlap if it can', () => { + const availableScroll: Position = patch(axis.line, 1); + const scroll: Position = subtract(maxDroppableScroll, availableScroll); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + scroll, + ); + // want to move 3 pixels + const request: Position = patch(axis.line, 3); + + autoScroller.onStateChange( + state.idle, + addDroppable(state.scrollJumpRequest(request), scrolled), + ); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + availableScroll, + ); + expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, 2)); + expect(mocks.move).not.toHaveBeenCalled(); }); it('should scroll the window by what it can, and move the rest', () => { - + // Setting the window scroll so it has a small amount of available space + const availableWindowScroll: Position = patch(axis.line, 2); + const maxWindowScroll: Position = getMaxScroll({ + scrollHeight: windowScrollSize.scrollHeight, + scrollWidth: windowScrollSize.scrollWidth, + height: viewport.height, + width: viewport.width, + }); + const windowScroll: Position = subtract(maxWindowScroll, availableWindowScroll); + setWindowScroll(windowScroll); + // Setting the droppable scroll so it has a small amount of available space + const availableDroppableScroll: Position = patch(axis.line, 1); + const droppableScroll: Position = subtract(maxDroppableScroll, availableDroppableScroll); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + droppableScroll, + ); + // How much we want to scroll + const request: Position = patch(axis.line, 5); + // How much we will not be able to absorb with droppable and window scroll + const remainder: Position = + subtract(subtract(request, availableDroppableScroll), availableWindowScroll); + const current = addDroppable(state.scrollJumpRequest(request), scrolled); + if (!current.drag) { + throw new Error('invalid state'); + } + const expectedManualMove: Position = + add(current.drag.current.client.selection, remainder); + + autoScroller.onStateChange(state.idle, current); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + availableDroppableScroll, + ); + expect(mocks.scrollWindow).toHaveBeenCalledWith(availableWindowScroll); + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expectedManualMove, + windowScroll, + true, + ); }); }); }); From 95d78126c12efd70f46573fb26f4256d5064f6af Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 13:00:11 +1100 Subject: [PATCH 072/163] more jump scrolling tests --- .../state/auto-scroll/jump-scroller.spec.js | 154 +++++++++++++++++- 1 file changed, 151 insertions(+), 3 deletions(-) diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index 0c2202759e..198111e4a5 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -8,9 +8,8 @@ import type { DroppableDimension, } from '../../../../src/types'; import type { AutoScroller } from '../../../../src/state/auto-scroll/auto-scroller-types'; -import type { PixelThresholds } from '../../../../src/state/auto-scroll/create-fluid-scroller'; import { getPixelThresholds, config } from '../../../../src/state/auto-scroll/create-fluid-scroller'; -import { add, patch, subtract } from '../../../../src/state/position'; +import { add, patch, subtract, negate } from '../../../../src/state/position'; import getArea from '../../../../src/state/get-area'; import setViewport, { resetViewport } from '../../../utils/set-viewport'; import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; @@ -365,7 +364,7 @@ describe('jump auto scrolling', () => { expect(mocks.move).not.toHaveBeenCalled(); }); - it('should scroll the window by what it can, and move the rest', () => { + it('should scroll the droppable and window by what it can, and manually move the rest', () => { // Setting the window scroll so it has a small amount of available space const availableWindowScroll: Position = patch(axis.line, 2); const maxWindowScroll: Position = getMaxScroll({ @@ -411,8 +410,157 @@ describe('jump auto scrolling', () => { }); }); }); + }); + + describe('moving backwards', () => { + describe('droppable is able to complete entire scroll', () => { + it('should only scroll the droppable', () => { + // move forward slightly to allow us to move forwards + const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 1)); + const request: Position = patch(axis.line, -1); + + autoScroller.onStateChange( + state.idle, + addDroppable(state.scrollJumpRequest(request), scrolled), + ); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + request, + ); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.move).not.toHaveBeenCalled(); + }); + }); + + describe('droppable is unable to complete the entire scroll', () => { + it('should manually move the entire request if it is unable to be partially completed by the window or the droppable', () => { + // scrollable cannot scroll backwards by default + disableWindowScroll(); + const request: Position = patch(axis.line, -1); + const current: State = state.scrollJumpRequest(request); + if (!current.drag) { + throw new Error('invalid state'); + } + const expected: Position = add(current.drag.current.client.selection, request); + + autoScroller.onStateChange( + state.idle, + addDroppable(current, scrollable), + ); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expected, + origin, + true, + ); + }); + + describe('window is unable to absorb some of the scroll', () => { + beforeEach(() => { + disableWindowScroll(); + }); + + it('should scroll the droppable what it can and move the rest', () => { + // able to scroll 1 pixel forward + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + patch(axis.line, 1), + ); + // want to move backwards 3 pixels + const request: Position = patch(axis.line, -3); + const current: State = state.scrollJumpRequest(request); + if (!current.drag) { + throw new Error('invalid state'); + } + // manual move will take what the droppable cannot + const expectedManualMove: Position = + add(current.drag.current.client.selection, patch(axis.line, -2)); + + autoScroller.onStateChange( + state.idle, + addDroppable(current, scrolled), + ); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + preset.home.descriptor.id, + // can only scroll backwards what it has! + patch(axis.line, -1), + ); + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expectedManualMove, + origin, + true, + ); + }); + }); + + describe('window can absorb some of the scroll', () => { + it('should scroll the entire overlap if it can', () => { + // let the window scroll be enough to move back into + setWindowScroll(patch(axis.line, 100)); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + patch(axis.line, 1), + ); + // want to move 3 pixels backwards + const request: Position = patch(axis.line, -3); + + autoScroller.onStateChange( + state.idle, + addDroppable(state.scrollJumpRequest(request), scrolled), + ); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + patch(axis.line, -1), + ); + expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, -2)); + expect(mocks.move).not.toHaveBeenCalled(); + }); + + it('should scroll the droppable and window by what it can, and manually move the rest', () => { + // Setting the window scroll so it has a small amount of available space + const windowScroll: Position = patch(axis.line, 2); + setWindowScroll(windowScroll); + // Setting the droppable scroll so it has a small amount of available space + const droppableScroll: Position = patch(axis.line, 1); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + droppableScroll, + ); + // How much we want to scroll + const request: Position = patch(axis.line, -5); + // How much we will not be able to absorb with droppable and window scroll + const remainder: Position = patch(axis.line, -2); + const current = addDroppable(state.scrollJumpRequest(request), scrolled); + if (!current.drag) { + throw new Error('invalid state'); + } + const expectedManualMove: Position = + add(current.drag.current.client.selection, remainder); + autoScroller.onStateChange(state.idle, current); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + negate(droppableScroll), + ); + expect(mocks.scrollWindow).toHaveBeenCalledWith(negate(windowScroll)); + expect(mocks.move).toHaveBeenCalledWith( + preset.inHome1.descriptor.id, + expectedManualMove, + windowScroll, + true, + ); + }); + }); + }); }); }); }); From c0ec77904bc3ff4f7ddf545368852bca2b8bf893 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 13:07:03 +1100 Subject: [PATCH 073/163] renaming files --- .../{auto-scroll => auto-scroller}/auto-scroller-types.js | 0 src/state/{auto-scroll => auto-scroller}/can-scroll.js | 0 .../fluid-scroller.js} | 0 .../get-scrollable-droppable-over.js | 0 .../auto-scroller.js => auto-scroller/index.js} | 4 ++-- .../is-too-big-to-auto-scroll.js | 0 .../jump-scroller.js} | 0 src/view/drag-drop-context/drag-drop-context.jsx | 4 ++-- test/unit/state/auto-scroll/can-scroll.spec.js | 2 +- test/unit/state/auto-scroll/fluid-scroller.spec.js | 8 ++++---- test/unit/state/auto-scroll/jump-scroller.spec.js | 5 ++--- 11 files changed, 11 insertions(+), 12 deletions(-) rename src/state/{auto-scroll => auto-scroller}/auto-scroller-types.js (100%) rename src/state/{auto-scroll => auto-scroller}/can-scroll.js (100%) rename src/state/{auto-scroll/create-fluid-scroller.js => auto-scroller/fluid-scroller.js} (100%) rename src/state/{auto-scroll => auto-scroller}/get-scrollable-droppable-over.js (100%) rename src/state/{auto-scroll/auto-scroller.js => auto-scroller/index.js} (89%) rename src/state/{auto-scroll => auto-scroller}/is-too-big-to-auto-scroll.js (100%) rename src/state/{auto-scroll/create-jump-scroller.js => auto-scroller/jump-scroller.js} (100%) diff --git a/src/state/auto-scroll/auto-scroller-types.js b/src/state/auto-scroller/auto-scroller-types.js similarity index 100% rename from src/state/auto-scroll/auto-scroller-types.js rename to src/state/auto-scroller/auto-scroller-types.js diff --git a/src/state/auto-scroll/can-scroll.js b/src/state/auto-scroller/can-scroll.js similarity index 100% rename from src/state/auto-scroll/can-scroll.js rename to src/state/auto-scroller/can-scroll.js diff --git a/src/state/auto-scroll/create-fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js similarity index 100% rename from src/state/auto-scroll/create-fluid-scroller.js rename to src/state/auto-scroller/fluid-scroller.js diff --git a/src/state/auto-scroll/get-scrollable-droppable-over.js b/src/state/auto-scroller/get-scrollable-droppable-over.js similarity index 100% rename from src/state/auto-scroll/get-scrollable-droppable-over.js rename to src/state/auto-scroller/get-scrollable-droppable-over.js diff --git a/src/state/auto-scroll/auto-scroller.js b/src/state/auto-scroller/index.js similarity index 89% rename from src/state/auto-scroll/auto-scroller.js rename to src/state/auto-scroller/index.js index ad093f63cd..2754c28e50 100644 --- a/src/state/auto-scroll/auto-scroller.js +++ b/src/state/auto-scroller/index.js @@ -1,6 +1,6 @@ // @flow -import createFluidScroller, { type FluidScroller } from './create-fluid-scroller'; -import createJumpScroller, { type JumpScroller } from './create-jump-scroller'; +import createFluidScroller, { type FluidScroller } from './fluid-scroller'; +import createJumpScroller, { type JumpScroller } from './jump-scroller'; import type { AutoScroller } from './auto-scroller-types'; import type { DraggableId, diff --git a/src/state/auto-scroll/is-too-big-to-auto-scroll.js b/src/state/auto-scroller/is-too-big-to-auto-scroll.js similarity index 100% rename from src/state/auto-scroll/is-too-big-to-auto-scroll.js rename to src/state/auto-scroller/is-too-big-to-auto-scroll.js diff --git a/src/state/auto-scroll/create-jump-scroller.js b/src/state/auto-scroller/jump-scroller.js similarity index 100% rename from src/state/auto-scroll/create-jump-scroller.js rename to src/state/auto-scroller/jump-scroller.js diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 74d8f1d3ae..de80a1ba4f 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -7,8 +7,8 @@ import createDimensionMarshal from '../../state/dimension-marshal/dimension-mars import createStyleMarshal from '../style-marshal/style-marshal'; import canStartDrag from '../../state/can-start-drag'; import scrollWindow from '../../window/scroll-window'; -import createAutoScroller from '../../state/auto-scroll/auto-scroller'; -import type { AutoScroller } from '../../state/auto-scroll/auto-scroller-types'; +import createAutoScroller from '../../state/auto-scroller'; +import type { AutoScroller } from '../../state/auto-scroller/auto-scroller-types'; import type { StyleMarshal } from '../style-marshal/style-marshal-types'; import type { DimensionMarshal, diff --git a/test/unit/state/auto-scroll/can-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js index 56c1e7c140..5907347a53 100644 --- a/test/unit/state/auto-scroll/can-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-scroll.spec.js @@ -10,7 +10,7 @@ import { getDroppableOverlap, canScrollDroppable, canScrollWindow, -} from '../../../../src/state/auto-scroll/can-scroll'; +} from '../../../../src/state/auto-scroller/can-scroll'; import { add, subtract } from '../../../../src/state/position'; import getArea from '../../../../src/state/get-area'; import { getPreset } from '../../../utils/dimension'; diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index 2622506164..b798b0c4a2 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -7,16 +7,16 @@ import type { DraggableDimension, DroppableDimension, } from '../../../../src/types'; -import type { AutoScroller } from '../../../../src/state/auto-scroll/auto-scroller-types'; -import type { PixelThresholds } from '../../../../src/state/auto-scroll/create-fluid-scroller'; -import { getPixelThresholds, config } from '../../../../src/state/auto-scroll/create-fluid-scroller'; +import type { AutoScroller } from '../../../../src/state/auto-scroller/auto-scroller-types'; +import type { PixelThresholds } from '../../../../src/state/auto-scroller/fluid-scroller'; +import { getPixelThresholds, config } from '../../../../src/state/auto-scroller/fluid-scroller'; import { add, patch, subtract } from '../../../../src/state/position'; import getArea from '../../../../src/state/get-area'; import setViewport, { resetViewport } from '../../../utils/set-viewport'; import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; import { vertical, horizontal } from '../../../../src/state/axis'; -import createAutoScroller from '../../../../src/state/auto-scroll/auto-scroller'; +import createAutoScroller from '../../../../src/state/auto-scroller/'; import * as state from '../../../utils/simple-state-preset'; import { getPreset } from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index 198111e4a5..8cbb177c28 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -7,15 +7,14 @@ import type { DraggableDimension, DroppableDimension, } from '../../../../src/types'; -import type { AutoScroller } from '../../../../src/state/auto-scroll/auto-scroller-types'; -import { getPixelThresholds, config } from '../../../../src/state/auto-scroll/create-fluid-scroller'; +import type { AutoScroller } from '../../../../src/state/auto-scroller/auto-scroller-types'; import { add, patch, subtract, negate } from '../../../../src/state/position'; import getArea from '../../../../src/state/get-area'; import setViewport, { resetViewport } from '../../../utils/set-viewport'; import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; import { vertical, horizontal } from '../../../../src/state/axis'; -import createAutoScroller from '../../../../src/state/auto-scroll/auto-scroller'; +import createAutoScroller from '../../../../src/state/auto-scroller'; import * as state from '../../../utils/simple-state-preset'; import { getPreset } from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; From 361cdd2a0a33d8bf1959e659175bfdcc00581a04 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 13:18:13 +1100 Subject: [PATCH 074/163] finalising jump move tests --- .../state/auto-scroll/jump-scroller.spec.js | 91 ++++++++++++++++++- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index 8cbb177c28..cfe9741acd 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -44,12 +44,22 @@ const addDroppable = (base: State, droppable: DroppableDimension): State => ({ }, }, }); +const addDraggable = (base: State, draggable: DraggableDimension): State => ({ + ...base, + dimension: { + ...base.dimension, + draggable: { + ...base.dimension.draggable, + [draggable.descriptor.id]: draggable, + }, + }, +}); const disableWindowScroll = () => { setWindowScrollSize({ scrollHeight: viewport.height, scrollWidth: viewport.width, - }) + }); }; describe('jump auto scrolling', () => { @@ -80,7 +90,40 @@ describe('jump auto scrolling', () => { describe('window scrolling', () => { it('should not scroll if the item is bigger than the viewport', () => { - + const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + const request: Position = patch(axis.line, 1); + const current: State = (() => { + const base: State = state.scrollJumpRequest(request); + + // $ExpectError - not checking for null + base.drag.initial.descriptor = tooBig.descriptor; + + return addDraggable(base, tooBig); + })(); + if (!current.drag) { + throw new Error('invalid state'); + } + const expected: Position = add(current.drag.current.client.selection, request); + + autoScroller.onStateChange(state.idle, current); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.move).toHaveBeenCalledWith( + tooBig.descriptor.id, + expected, + origin, + true, + ); }); describe('moving forwards', () => { @@ -245,8 +288,48 @@ describe('jump auto scrolling', () => { const maxDroppableScroll: Position = scrollable.viewport.closestScrollable.scroll.max; - it('should not scroll if the item is bigger than the viewport', () => { - + it('should not scroll if the item is bigger than the droppable', () => { + const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); + // setting viewport to be bigger + setViewport(getArea({ + top: 0, + left: 0, + right: 10000, + bottom: 10000, + })); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + const request: Position = patch(axis.line, 1); + const current: State = (() => { + const base: State = state.scrollJumpRequest(request); + + // $ExpectError - not checking for null + base.drag.initial.descriptor = tooBig.descriptor; + + return addDroppable(addDraggable(base, tooBig), scrollable); + })(); + if (!current.drag) { + throw new Error('invalid state'); + } + const expected: Position = add(current.drag.current.client.selection, request); + + autoScroller.onStateChange(state.idle, current); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.move).toHaveBeenCalledWith( + tooBig.descriptor.id, + expected, + origin, + true, + ); }); describe('moving forwards', () => { From 23cf7f2ff1169f1c7f85ca95d23d0d2918fd06af Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 20:06:59 +1100 Subject: [PATCH 075/163] trying something different --- src/state/auto-scroller/fluid-scroller.js | 2 ++ src/state/auto-scroller/index.js | 1 + src/state/auto-scroller/jump-scroller.js | 14 +++++------ src/state/dimension.js | 24 +++++++++---------- src/state/get-droppable-over.js | 2 +- .../drag-drop-context/drag-drop-context.jsx | 11 +++++++-- .../droppable-dimension-publisher.jsx | 11 +++++---- src/window/get-window-scroll.js | 1 - 8 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index e039799de5..8c621bb0c0 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -169,8 +169,10 @@ export default ({ } const requiredWindowScroll: ?Position = getRequiredScroll(viewport, center); + console.log('required scroll', requiredWindowScroll); if (requiredWindowScroll && canScrollWindow(requiredWindowScroll)) { + console.log('schedling window scroll'); scheduleWindowScroll(requiredWindowScroll); return; } diff --git a/src/state/auto-scroller/index.js b/src/state/auto-scroller/index.js index 2754c28e50..c7c7541a61 100644 --- a/src/state/auto-scroller/index.js +++ b/src/state/auto-scroller/index.js @@ -37,6 +37,7 @@ export default ({ }); const onStateChange = (previous: State, current: State): void => { + return; // now dragging if (current.phase === 'DRAGGING') { if (!current.drag) { diff --git a/src/state/auto-scroller/jump-scroller.js b/src/state/auto-scroller/jump-scroller.js index 2a1db42c49..d318acc0d8 100644 --- a/src/state/auto-scroller/jump-scroller.js +++ b/src/state/auto-scroller/jump-scroller.js @@ -1,6 +1,6 @@ // @flow import { add, subtract } from '../position'; -import getWindowScrollPosition from '../../window/get-window-scroll'; +import getWindowScroll from '../../window/get-window-scroll'; import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; import getViewport from '../../window/get-viewport'; import { @@ -47,7 +47,7 @@ export default ({ } const client: Position = add(drag.current.client.selection, offset); - move(drag.initial.descriptor.id, client, getWindowScrollPosition(), true); + move(drag.initial.descriptor.id, client, getWindowScroll(), true); }; const jumpScroller: JumpScroller = (state: State) => { @@ -125,12 +125,12 @@ export default ({ } // window can only partially absorb overlap - // need to move the item by the remainder and scroll the window - moveByOffset(state, windowOverlap); - // the amount that the window can actually scroll const whatTheWindowCanScroll: Position = subtract(overlap, windowOverlap); scrollWindow(whatTheWindowCanScroll); + + // need to move the item by the remainder and scroll the window + moveByOffset(state, windowOverlap); return; } @@ -150,10 +150,10 @@ export default ({ return; } - moveByOffset(state, overlap); - // the amount that the window can actually scroll const whatTheWindowCanScroll: Position = subtract(request, overlap); scrollWindow(whatTheWindowCanScroll); + // manually move to the rest + moveByOffset(state, overlap); }; return jumpScroller; diff --git a/src/state/dimension.js b/src/state/dimension.js index abfb697a79..051e3d0e23 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -1,7 +1,7 @@ // @flow import { vertical, horizontal } from './axis'; import getArea from './get-area'; -import { offsetByPosition, expandBySpacing } from './spacing'; +import { offsetByPosition, expandBySpacing, expandByPosition } from './spacing'; import { subtract, negate } from './position'; import getMaxScroll from './get-max-scroll'; import type { @@ -69,7 +69,7 @@ type GetDroppableArgs = {| descriptor: DroppableDescriptor, client: Area, // optionally provided - and can also be null - closest?: {| + closest?: ?{| frameClient: Area, scrollWidth: number, scrollHeight: number, @@ -163,21 +163,21 @@ export const getDroppableDimension = ({ windowScroll = origin, isEnabled = true, }: GetDroppableArgs): DroppableDimension => { - const withMargin: Spacing = expandBySpacing(client, margin); - const withWindowScroll: Spacing = offsetByPosition(client, windowScroll); - // If no frameClient is provided, or if the area matches the frameClient, this - // droppable is its own container. In this case we include its margin in the container bounds. - // Otherwise, the container is a scrollable parent. In this case we don't care about margins - // in the container bounds. + // the displacement (shift) of scroll is its negation + const windowScrollDisplacement: Position = negate(windowScroll); - const subject: Area = getArea(expandBySpacing(withWindowScroll, margin)); + const withMargin: Spacing = expandBySpacing(client, margin); + const withWindowScrollDisplacement: Spacing = offsetByPosition(client, windowScrollDisplacement); + const subject: Area = getArea(expandBySpacing(withWindowScrollDisplacement, margin)); const closestScrollable: ?ClosestScrollable = (() => { if (!closest) { return null; } - const frame: Area = getArea(offsetByPosition(closest.frameClient, windowScroll)); + const frame: Area = getArea(offsetByPosition(closest.frameClient, windowScrollDisplacement)); + console.log('frame', frame); + console.log('frame client', closest.frameClient); const maxScroll: Position = getMaxScroll({ scrollHeight: closest.scrollHeight, @@ -224,10 +224,10 @@ export const getDroppableDimension = ({ withMarginAndPadding: getArea(expandBySpacing(withMargin, padding)), }, page: { - withoutMargin: getArea(withWindowScroll), + withoutMargin: getArea(withWindowScrollDisplacement), withMargin: subject, withMarginAndPadding: - getArea(expandBySpacing(withWindowScroll, expandBySpacing(margin, padding))), + getArea(expandBySpacing(withWindowScrollDisplacement, expandBySpacing(margin, padding))), }, viewport, }; diff --git a/src/state/get-droppable-over.js b/src/state/get-droppable-over.js index 9d89127349..dd585da53c 100644 --- a/src/state/get-droppable-over.js +++ b/src/state/get-droppable-over.js @@ -99,7 +99,7 @@ const getClippedAreaWithPlaceholder = ({ return clipped; } - const subjectWithGrowth = getWithGrowth(clipped, requiredGrowth); + const subjectWithGrowth: Area = getWithGrowth(clipped, requiredGrowth); const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; // The droppable has no scroll container diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index de80a1ba4f..e9562deb63 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -114,14 +114,21 @@ export default class DragDropContext extends React.Component { }; this.dimensionMarshal = createDimensionMarshal(callbacks); this.autoScroller = createAutoScroller({ - scrollWindow, - scrollDroppable: this.dimensionMarshal.scrollDroppable, + scrollWindow: (...args) => { + console.warn('WINDOW SCROLL', ...args); + scrollWindow(...args); + }, + scrollDroppable: (...args) => { + console.warn('DROPPABLE SCROLL', ...args); + this.dimensionMarshal.scrollDroppable(...args); + }, move: ( id: DraggableId, client: Position, windowScroll: Position, shouldAnimate?: boolean ): void => { + console.warn('MOVE:', client); this.store.dispatch(move(id, client, windowScroll, shouldAnimate)); }, }); diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 3bd833b8a6..7c0cca05b1 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -4,7 +4,7 @@ import type { Node } from 'react'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import rafSchedule from 'raf-schd'; -import getWindowScrollPosition from '../../window/get-window-scroll'; +import getWindowScroll from '../../window/get-window-scroll'; import getArea from '../../state/get-area'; import { getDroppableDimension } from '../../state/dimension'; import getClosestScrollable from '../get-closest-scrollable'; @@ -105,6 +105,11 @@ export default class DroppableDimensionPublisher extends Component { const offset: Position = this.getClosestScroll(); this.memoizedUpdateScroll(offset.x, offset.y); }); + // scheduleScrollUpdate = () => { + // // Capturing the scroll now so that it is the latest value + // const offset: Position = this.getClosestScroll(); + // this.memoizedUpdateScroll(offset.x, offset.y); + // }; onClosestScroll = () => this.scheduleScrollUpdate(); @@ -291,8 +296,6 @@ export default class DroppableDimensionPublisher extends Component { const scrollWidth: number = closestScrollable.scrollWidth; const scrollHeight: number = closestScrollable.scrollHeight; - console.log('frameClient', frameClient); - return { frameClient, scrollWidth, @@ -309,7 +312,7 @@ export default class DroppableDimensionPublisher extends Component { closest, margin, padding, - windowScroll: getWindowScrollPosition(), + windowScroll: getWindowScroll(), isEnabled: !isDropDisabled, }); diff --git a/src/window/get-window-scroll.js b/src/window/get-window-scroll.js index abec8422c2..64e39a1433 100644 --- a/src/window/get-window-scroll.js +++ b/src/window/get-window-scroll.js @@ -1,5 +1,4 @@ // @flow -import { apply } from '../state/position'; import type { Position } from '../types'; // The browsers update document.documentElement.scrollTop and window.pageYOffset From fcd3296be99701e3741a8af497dd659dcccfaa4c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 20:32:35 +1100 Subject: [PATCH 076/163] fixing scroll target --- src/state/dimension.js | 15 +++++---------- .../droppable-dimension-publisher.jsx | 18 +----------------- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/src/state/dimension.js b/src/state/dimension.js index 051e3d0e23..a460fab349 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -163,21 +163,16 @@ export const getDroppableDimension = ({ windowScroll = origin, isEnabled = true, }: GetDroppableArgs): DroppableDimension => { - // the displacement (shift) of scroll is its negation - const windowScrollDisplacement: Position = negate(windowScroll); - const withMargin: Spacing = expandBySpacing(client, margin); - const withWindowScrollDisplacement: Spacing = offsetByPosition(client, windowScrollDisplacement); - const subject: Area = getArea(expandBySpacing(withWindowScrollDisplacement, margin)); + const withWindowScroll: Spacing = offsetByPosition(client, windowScroll); + const subject: Area = getArea(expandBySpacing(withWindowScroll, margin)); const closestScrollable: ?ClosestScrollable = (() => { if (!closest) { return null; } - const frame: Area = getArea(offsetByPosition(closest.frameClient, windowScrollDisplacement)); - console.log('frame', frame); - console.log('frame client', closest.frameClient); + const frame: Area = getArea(offsetByPosition(closest.frameClient, windowScroll)); const maxScroll: Position = getMaxScroll({ scrollHeight: closest.scrollHeight, @@ -224,10 +219,10 @@ export const getDroppableDimension = ({ withMarginAndPadding: getArea(expandBySpacing(withMargin, padding)), }, page: { - withoutMargin: getArea(withWindowScrollDisplacement), + withoutMargin: getArea(withWindowScroll), withMargin: subject, withMarginAndPadding: - getArea(expandBySpacing(withWindowScrollDisplacement, expandBySpacing(margin, padding))), + getArea(expandBySpacing(withWindowScroll, expandBySpacing(margin, padding))), }, viewport, }; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 7c0cca05b1..4466eac665 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -37,22 +37,6 @@ type Props = {| |} const origin: Position = { x: 0, y: 0 }; -const floor = apply(Math.floor); - -const getSafeScrollArea = (el: HTMLElement): Area => { - const top: number = el.offsetTop; - const left: number = el.offsetLeft; - const width: number = el.clientWidth; - const height: number = el.clientHeight; - - // computed - const right: number = left + width; - const bottom: number = top + height; - - return getArea({ - top, left, right, bottom, - }); -}; export default class DroppableDimensionPublisher extends Component { /* eslint-disable react/sort-comp */ @@ -291,7 +275,7 @@ export default class DroppableDimensionPublisher extends Component { return null; } - const frameClient: Area = getSafeScrollArea((closestScrollable: any)); + const frameClient: Area = getArea(closestScrollable.getBoundingClientRect()); const scroll: Position = this.getClosestScroll(); const scrollWidth: number = closestScrollable.scrollWidth; const scrollHeight: number = closestScrollable.scrollHeight; From 7b3734f3df3e1f0d2d36f58c4b56e5bc8d93e05c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 20:40:10 +1100 Subject: [PATCH 077/163] removing logs --- src/state/auto-scroller/fluid-scroller.js | 2 -- src/state/auto-scroller/index.js | 1 - 2 files changed, 3 deletions(-) diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index 8c621bb0c0..e039799de5 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -169,10 +169,8 @@ export default ({ } const requiredWindowScroll: ?Position = getRequiredScroll(viewport, center); - console.log('required scroll', requiredWindowScroll); if (requiredWindowScroll && canScrollWindow(requiredWindowScroll)) { - console.log('schedling window scroll'); scheduleWindowScroll(requiredWindowScroll); return; } diff --git a/src/state/auto-scroller/index.js b/src/state/auto-scroller/index.js index c7c7541a61..2754c28e50 100644 --- a/src/state/auto-scroller/index.js +++ b/src/state/auto-scroller/index.js @@ -37,7 +37,6 @@ export default ({ }); const onStateChange = (previous: State, current: State): void => { - return; // now dragging if (current.phase === 'DRAGGING') { if (!current.drag) { From a4d890c9a5f4d6a392f14069edaa526a1790678a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 7 Feb 2018 21:05:04 +1100 Subject: [PATCH 078/163] removing some logging --- src/view/drag-drop-context/drag-drop-context.jsx | 11 ++--------- .../droppable-dimension-publisher.jsx | 2 -- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index e9562deb63..de80a1ba4f 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -114,21 +114,14 @@ export default class DragDropContext extends React.Component { }; this.dimensionMarshal = createDimensionMarshal(callbacks); this.autoScroller = createAutoScroller({ - scrollWindow: (...args) => { - console.warn('WINDOW SCROLL', ...args); - scrollWindow(...args); - }, - scrollDroppable: (...args) => { - console.warn('DROPPABLE SCROLL', ...args); - this.dimensionMarshal.scrollDroppable(...args); - }, + scrollWindow, + scrollDroppable: this.dimensionMarshal.scrollDroppable, move: ( id: DraggableId, client: Position, windowScroll: Position, shouldAnimate?: boolean ): void => { - console.warn('MOVE:', client); this.store.dispatch(move(id, client, windowScroll, shouldAnimate)); }, }); diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 4466eac665..40c62f1dd4 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -106,8 +106,6 @@ export default class DroppableDimensionPublisher extends Component { console.warn('Updating Droppable scroll while not watching for updates'); } - console.log('DroppableDimensionPublisher: now scrolling', change); - this.closestScrollable.scrollTop += change.y; this.closestScrollable.scrollLeft += change.x; } From 1955e40679b75290fc1f1258c243ed0905f42df5 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 07:54:57 +1100 Subject: [PATCH 079/163] initial --- src/state/fire-hooks.js | 253 +++++++++++------- src/types.js | 11 +- .../drag-drop-context/drag-drop-context.jsx | 1 + 3 files changed, 158 insertions(+), 107 deletions(-) diff --git a/src/state/fire-hooks.js b/src/state/fire-hooks.js index fca52f5637..b2a97aab8f 100644 --- a/src/state/fire-hooks.js +++ b/src/state/fire-hooks.js @@ -1,143 +1,194 @@ // @flow +import memoizeOne from 'memoize-one'; import type { + Announce, State, Hooks, DragStart, DropResult, + DraggableId, + DroppableId, + TypeId, DraggableLocation, DraggableDescriptor, DroppableDimension, } from '../types'; -export default (hooks: Hooks, previous: State, current: State): void => { - const { onDragStart, onDragEnd } = hooks; - const currentPhase = current.phase; - const previousPhase = previous.phase; +const announce: Announce = (message: string) => + console.log(`%c ${message}`, 'color: green; font-size: 20px;'); - // Exit early if phase in unchanged - if (currentPhase === previousPhase) { - return; - } - - // Drag start - if (currentPhase === 'DRAGGING' && previousPhase !== 'DRAGGING') { - // onDragStart is optional - if (!onDragStart) { - return; - } - - if (!current.drag) { - console.error('cannot fire onDragStart hook without drag state', { current, previous }); - return; - } - - const descriptor: DraggableDescriptor = current.drag.initial.descriptor; - const home: ?DroppableDimension = current.dimension.droppable[descriptor.droppableId]; - - if (!home) { - console.error('cannot find dimension for home droppable'); - return; - } +type State = {| + hasMovedFromStartLocation: boolean, +|} +export default () => { + const getMemoizedDragStart = memoizeOne(( + draggableId: DraggableId, + droppableId: DroppableId, + type: TypeId, + index: number, + ): DragStart => { const source: DraggableLocation = { - index: descriptor.index, - droppableId: descriptor.droppableId, + index, + droppableId, }; const start: DragStart = { - draggableId: descriptor.id, - type: home.descriptor.type, + draggableId, + type, source, }; - onDragStart(start); - return; - } + return start; + }); - // Drag end - if (currentPhase === 'DROP_COMPLETE' && previousPhase !== 'DROP_COMPLETE') { - if (!current.drop || !current.drop.result) { - console.error('cannot fire onDragEnd hook without drag state', { current, previous }); - return; - } + const getMemoizeDragResult = memoizeOne((): DropResult => { - const { - source, - destination, - draggableId, - type, - } = current.drop.result; + }); - // Could be a cancel or a drop nowhere - if (!destination) { - onDragEnd(current.drop.result); - return; + const getDragStart = (state: State): ?DragStart => { + if (!state.drag) { + return null; } - // Do not publish a result.destination where nothing moved - const didMove: boolean = source.droppableId !== destination.droppableId || - source.index !== destination.index; + const descriptor: DraggableDescriptor = state.drag.initial.descriptor; + const home: ?DroppableDimension = state.dimension.droppable[descriptor.droppableId]; - if (didMove) { - onDragEnd(current.drop.result); + if (!home) { + return null; + } + + return getMemoizedDragStart( + descriptor.id, + descriptor.droppableId, + home.descriptor.type, + descriptor.index, + ); + }; + + const onPhaseChange = (hooks: Hooks, previous: State, current: State): void => { + const { onDragStart, onDragUpdate, onDragEnd } = hooks; + const currentPhase = current.phase; + const previousPhase = previous.phase; + + // Exit early if phase in unchanged + if (currentPhase === previousPhase) { return; } - const muted: DropResult = { - draggableId, - type, - source, - destination: null, - }; + // Drag start + if (currentPhase === 'DRAGGING' && previousPhase !== 'DRAGGING') { + // onDragStart is optional + if (!onDragStart) { + return; + } + + const start: ?DragStart = getDragStart(current); - onDragEnd(muted); - return; - } + if (!start) { + console.error('Unable to publish onDragStart'); + return; + } - // Drag ended while dragging - if (currentPhase === 'IDLE' && previousPhase === 'DRAGGING') { - if (!previous.drag) { - console.error('cannot fire onDragEnd for cancel because cannot find previous drag'); + onDragStart(start, announce); return; } - const descriptor: DraggableDescriptor = previous.drag.initial.descriptor; - const home: ?DroppableDimension = previous.dimension.droppable[descriptor.droppableId]; + // Dragging continuing + if (currentPhase === 'DRAGGING' && previousPhase === 'DRAGGING') { + // only call the onDragUpdate hook if something has changed from last time + if (!onDragUpdate) { + return; + } - if (!home) { - console.error('cannot find dimension for home droppable'); - return; + onDragUpdate(start, ) } - const source: DraggableLocation = { - index: descriptor.index, - droppableId: descriptor.droppableId, - }; + // Drag end + if (currentPhase === 'DROP_COMPLETE' && previousPhase !== 'DROP_COMPLETE') { + if (!current.drop || !current.drop.result) { + console.error('cannot fire onDragEnd hook without drag state', { current, previous }); + return; + } - const result: DropResult = { - draggableId: descriptor.id, - type: home.descriptor.type, + const { source, - destination: null, - }; - onDragEnd(result); - return; - } - - // Drag ended during a drop animation. Not super sure how this can even happen. - // This is being really safe - if (currentPhase === 'IDLE' && previousPhase === 'DROP_ANIMATING') { - if (!previous.drop || !previous.drop.pending) { - console.error('cannot fire onDragEnd for cancel because cannot find previous pending drop'); + destination, + draggableId, + type, + } = current.drop.result; + + // Could be a cancel or a drop nowhere + if (!destination) { + onDragEnd(current.drop.result, announce); + return; + } + + // Do not publish a result.destination where nothing moved + const didMove: boolean = source.droppableId !== destination.droppableId || + source.index !== destination.index; + + if (didMove) { + onDragEnd(current.drop.result, announce); + return; + } + + const muted: DropResult = { + draggableId, + type, + source, + destination: null, + }; + + onDragEnd(muted, announce); return; } - const result: DropResult = { - draggableId: previous.drop.pending.result.draggableId, - type: previous.drop.pending.result.type, - source: previous.drop.pending.result.source, - destination: null, - }; - onDragEnd(result); - } -}; + // Drag ended while dragging + if (currentPhase === 'IDLE' && previousPhase === 'DRAGGING') { + if (!previous.drag) { + console.error('cannot fire onDragEnd for cancel because cannot find previous drag'); + return; + } + + const descriptor: DraggableDescriptor = previous.drag.initial.descriptor; + const home: ?DroppableDimension = previous.dimension.droppable[descriptor.droppableId]; + + if (!home) { + console.error('cannot find dimension for home droppable'); + return; + } + + const source: DraggableLocation = { + index: descriptor.index, + droppableId: descriptor.droppableId, + }; + + const result: DropResult = { + draggableId: descriptor.id, + type: home.descriptor.type, + source, + destination: null, + }; + onDragEnd(result, announce); + return; + } + + // Drag ended during a drop animation. Not super sure how this can even happen. + // This is being really safe + if (currentPhase === 'IDLE' && previousPhase === 'DROP_ANIMATING') { + if (!previous.drop || !previous.drop.pending) { + console.error('cannot fire onDragEnd for cancel because cannot find previous pending drop'); + return; + } + + const result: DropResult = { + draggableId: previous.drop.pending.result.draggableId, + type: previous.drop.pending.result.type, + source: previous.drop.pending.result.source, + destination: null, + }; + onDragEnd(result, announce); + } + }; +} diff --git a/src/types.js b/src/types.js index 13d572790e..28052d03c8 100644 --- a/src/types.js +++ b/src/types.js @@ -224,10 +224,6 @@ export type CurrentDrag = {| shouldAnimate: boolean, |} -// type PreviousDrag = { -// droppableOverId: ?DroppableId, -// }; - // published when a drag starts export type DragStart = {| draggableId: DraggableId, @@ -316,7 +312,10 @@ export type Action = ActionCreators; export type Dispatch = ReduxDispatch; export type Store = ReduxStore; +export type Announce = (message: string) => void; + export type Hooks = {| - onDragStart?: (start: DragStart) => void, - onDragEnd: (result: DropResult) => void, + onDragStart?: (start: DragStart, announce: Announce) => void, + onDragUpdate?: (current: DropResult, announce: Announce) => void, + onDragEnd: (result: DropResult, announce: Announce) => void, |} diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index de80a1ba4f..210c07e020 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -153,6 +153,7 @@ export default class DragDropContext extends React.Component { const hooks: Hooks = { onDragStart: this.props.onDragStart, onDragEnd: this.props.onDragEnd, + onDragUpdate: this.props.onDragUpdate, }; fireHooks(hooks, previous, current); From 273645c31ca3ccf0c741e09ca77ae63d0c073d53 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 08:20:08 +1100 Subject: [PATCH 080/163] wirering --- src/index.js | 1 + .../{fire-hooks.js => hooks/hook-caller.js} | 74 ++++++++++++++----- src/state/hooks/hooks-types.js | 17 +++++ src/types.js | 10 +-- .../drag-drop-context/drag-drop-context.jsx | 51 ++++++------- stories/src/vertical/quote-app.jsx | 16 +++- 6 files changed, 112 insertions(+), 57 deletions(-) rename src/state/{fire-hooks.js => hooks/hook-caller.js} (78%) create mode 100644 src/state/hooks/hooks-types.js diff --git a/src/index.js b/src/index.js index 6562477118..5f1c1fee3f 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ export type { DragStart, DropResult, DraggableLocation, + Announce, } from './types'; // Droppable diff --git a/src/state/fire-hooks.js b/src/state/hooks/hook-caller.js similarity index 78% rename from src/state/fire-hooks.js rename to src/state/hooks/hook-caller.js index b2a97aab8f..2569c376de 100644 --- a/src/state/fire-hooks.js +++ b/src/state/hooks/hook-caller.js @@ -1,9 +1,10 @@ // @flow import memoizeOne from 'memoize-one'; +import type { Hooks, HookCaller } from './hooks-types'; import type { Announce, State, - Hooks, + DragState, DragStart, DropResult, DraggableId, @@ -12,16 +13,16 @@ import type { DraggableLocation, DraggableDescriptor, DroppableDimension, -} from '../types'; +} from '../../types'; -const announce: Announce = (message: string) => - console.log(`%c ${message}`, 'color: green; font-size: 20px;'); +// const announce: Announce = (message: string) => +// console.log(`%c ${message}`, 'color: green; font-size: 20px;'); -type State = {| - hasMovedFromStartLocation: boolean, -|} +// type State = {| +// hasMovedFromStartLocation: boolean, +// |} -export default () => { +export default (announce: Announce): HookCaller => { const getMemoizedDragStart = memoizeOne(( draggableId: DraggableId, droppableId: DroppableId, @@ -66,12 +67,47 @@ export default () => { ); }; - const onPhaseChange = (hooks: Hooks, previous: State, current: State): void => { + const onStateChange = (hooks: Hooks, previous: State, current: State): void => { const { onDragStart, onDragUpdate, onDragEnd } = hooks; const currentPhase = current.phase; const previousPhase = previous.phase; - // Exit early if phase in unchanged + // Dragging in progress + if (currentPhase === 'DRAGGING' && previousPhase === 'DRAGGING') { + // only call the onDragUpdate hook if something has changed from last time + if (!onDragUpdate) { + return; + } + + const start: ?DragStart = getDragStart(current); + + if (!start) { + console.error('Cannot update drag when there is invalid state'); + return; + } + + const drag: ?DragState = current.drag; + + if (!drag) { + console.error('Cannot update drag when there is invalid state'); + return; + } + + const destination: ?DraggableLocation = drag.impact.destination; + + const result: DropResult = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination, + }; + + onDragUpdate(result, announce); + return; + } + + // From this point we only care about phase changes + if (currentPhase === previousPhase) { return; } @@ -94,15 +130,7 @@ export default () => { return; } - // Dragging continuing - if (currentPhase === 'DRAGGING' && previousPhase === 'DRAGGING') { - // only call the onDragUpdate hook if something has changed from last time - if (!onDragUpdate) { - return; - } - onDragUpdate(start, ) - } // Drag end if (currentPhase === 'DROP_COMPLETE' && previousPhase !== 'DROP_COMPLETE') { @@ -112,11 +140,11 @@ export default () => { } const { - source, + source, destination, draggableId, type, - } = current.drop.result; + } = current.drop.result; // Could be a cancel or a drop nowhere if (!destination) { @@ -191,4 +219,10 @@ export default () => { onDragEnd(result, announce); } }; + + const caller: HookCaller = { + onStateChange, + }; + + return caller; } diff --git a/src/state/hooks/hooks-types.js b/src/state/hooks/hooks-types.js new file mode 100644 index 0000000000..31fd5d4099 --- /dev/null +++ b/src/state/hooks/hooks-types.js @@ -0,0 +1,17 @@ +// @flow +import type { + DragStart, + DropResult, + Announce, + State, +} from '../../types'; + +export type Hooks = {| + onDragStart?: (start: DragStart, announce: Announce) => void, + onDragUpdate?: (current: DropResult, announce: Announce) => void, + onDragEnd: (result: DropResult, announce: Announce) => void, +|} + +export type HookCaller = {| + onStateChange: (hooks: Hooks, previous: State, current: State) => void, +|} diff --git a/src/types.js b/src/types.js index 28052d03c8..2c5f821b16 100644 --- a/src/types.js +++ b/src/types.js @@ -233,9 +233,7 @@ export type DragStart = {| // published when a drag finishes export type DropResult = {| - draggableId: DraggableId, - type: TypeId, - source: DraggableLocation, + ...DragStart, // may not have any destination (drag to nowhere) destination: ?DraggableLocation, |} @@ -313,9 +311,3 @@ export type Dispatch = ReduxDispatch; export type Store = ReduxStore; export type Announce = (message: string) => void; - -export type Hooks = {| - onDragStart?: (start: DragStart, announce: Announce) => void, - onDragUpdate?: (current: DropResult, announce: Announce) => void, - onDragEnd: (result: DropResult, announce: Announce) => void, -|} diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 210c07e020..f575863caa 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -2,7 +2,7 @@ import React, { type Node } from 'react'; import PropTypes from 'prop-types'; import createStore from '../../state/create-store'; -import fireHooks from '../../state/fire-hooks'; +import createHookCaller from '../../state/hooks/hook-caller'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; import createStyleMarshal from '../style-marshal/style-marshal'; import canStartDrag from '../../state/can-start-drag'; @@ -18,12 +18,15 @@ import type { DraggableId, Store, State, - Hooks, DraggableDimension, DroppableDimension, DroppableId, Position, } from '../../types'; +import type { + HookCaller, + Hooks, +} from '../../state/hooks/hooks-types'; import { storeKey, dimensionMarshalKey, @@ -54,6 +57,7 @@ export default class DragDropContext extends React.Component { dimensionMarshal: DimensionMarshal styleMarshal: StyleMarshal autoScroller: AutoScroller + hookCaller: HookCaller unsubscribe: Function // Need to declare childContextTypes without flow @@ -91,6 +95,11 @@ export default class DragDropContext extends React.Component { componentWillMount() { this.store = createStore(); + // create the hook caller + this.hookCaller = createHookCaller( + (message: string) => console.log(`%c ${message}`, 'color: green; font-size: 20px;') + ); + // create the style marshal this.styleMarshal = createStyleMarshal(); @@ -135,36 +144,28 @@ export default class DragDropContext extends React.Component { // functions synchronously trigger more updates previous = current; + // TODO: this probs needs to be done first + const hooks: Hooks = { + onDragStart: this.props.onDragStart, + onDragEnd: this.props.onDragEnd, + onDragUpdate: this.props.onDragUpdate, + }; + this.hookCaller.onStateChange(hooks, previous, current); + if (current.phase !== previousValue.phase) { // executing phase change handlers first - this.onPhaseChange(previousValue, current); + // Update the global styles + this.styleMarshal.onPhaseChange(current); + + // inform the dimension marshal about updates + // this can trigger more actions synchronously so we are placing it last + this.dimensionMarshal.onPhaseChange(current); } - // TODO: should this take the latest previous to prevent scroll post drop? - this.onStateChange(previousValue, current); + this.autoScroller.onStateChange(previous, current); }); } - onStateChange(previous: State, current: State) { - this.autoScroller.onStateChange(previous, current); - } - - onPhaseChange(previous: State, current: State) { - const hooks: Hooks = { - onDragStart: this.props.onDragStart, - onDragEnd: this.props.onDragEnd, - onDragUpdate: this.props.onDragUpdate, - }; - fireHooks(hooks, previous, current); - - // Update the global styles - this.styleMarshal.onPhaseChange(current); - - // inform the dimension marshal about updates - // this can trigger more actions synchronously so we are placing it last - this.dimensionMarshal.onPhaseChange(current); - } - componentDidMount() { // need to mount the style marshal after we are in the dom // this cannot be done before otherwise it would break diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index 4af5d62d4d..e2fa5220a7 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -7,7 +7,7 @@ import QuoteList from '../primatives/quote-list'; import { colors, grid } from '../constants'; import reorder from '../reorder'; import type { Quote } from '../types'; -import type { DropResult, DragStart } from '../../../src/types'; +import type { DropResult, DragStart, Announce } from '../../../src/types'; const publishOnDragStart = action('onDragStart'); const publishOnDragEnd = action('onDragEnd'); @@ -40,7 +40,9 @@ export default class QuoteApp extends Component { quotes: this.props.initial, }; - onDragStart = (initial: DragStart) => { + onDragStart = (initial: DragStart, announce: Announce) => { + announce('drag start!'); + console.log('initial', initial); publishOnDragStart(initial); // Add a little vibration if the browser supports it. // Add's a nice little physical feedback @@ -49,7 +51,14 @@ export default class QuoteApp extends Component { } } - onDragEnd = (result: DropResult) => { + onDragUpdate = (current: DropResult, announce: Announce) => { + announce('update!'); + console.log('current', current); + } + + onDragEnd = (result: DropResult, announce: Announce) => { + announce('on drop!'); + console.log('result', result); publishOnDragEnd(result); // dropped outside the list @@ -74,6 +83,7 @@ export default class QuoteApp extends Component { return ( From ff0d8930ffbc55107f85daad4370f416e833bb8d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 11:18:10 +1100 Subject: [PATCH 081/163] fixing hook tests --- src/state/hooks/hook-caller.js | 158 ++++++++++++------ ...fire-hooks.spec.js => hook-caller.spec.js} | 48 +++--- 2 files changed, 135 insertions(+), 71 deletions(-) rename test/unit/state/{fire-hooks.spec.js => hook-caller.spec.js} (80%) diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index 2569c376de..98f9e0b7f5 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -1,9 +1,8 @@ // @flow -import memoizeOne from 'memoize-one'; import type { Hooks, HookCaller } from './hooks-types'; import type { Announce, - State, + State as AppState, DragState, DragStart, DropResult, @@ -18,56 +17,82 @@ import type { // const announce: Announce = (message: string) => // console.log(`%c ${message}`, 'color: green; font-size: 20px;'); -// type State = {| -// hasMovedFromStartLocation: boolean, -// |} +type State = { + isDragging: boolean, + start: ?DraggableLocation, + lastDestination: ?DraggableLocation, + hasMovedFromStartLocation: boolean, +} + +const initial: State = { + isDragging: false, + start: null, + lastDestination: null, + hasMovedFromStartLocation: false, +}; + +const areLocationsEqual = (current: ?DraggableLocation, next: ?DraggableLocation) => { + // if both are null - we are equal + if (current == null && next == null) { + return true; + } + + // if one is null - then they are not equal + if (current == null || next == null) { + return false; + } + + // compare their actual values + return current.droppableId === next.droppableId && + current.index === next.index; +}; export default (announce: Announce): HookCaller => { - const getMemoizedDragStart = memoizeOne(( - draggableId: DraggableId, - droppableId: DroppableId, - type: TypeId, - index: number, - ): DragStart => { - const source: DraggableLocation = { - index, - droppableId, - }; + let state: State = initial; - const start: DragStart = { - draggableId, - type, - source, + const setState = (partial: Object): void => { + const newState: State = { + ...state, + ...partial, }; + state = newState; + }; - return start; - }); - - const getMemoizeDragResult = memoizeOne((): DropResult => { - - }); - - const getDragStart = (state: State): ?DragStart => { - if (!state.drag) { + const getDragStart = (appState: AppState): ?DragStart => { + if (!appState.drag) { return null; } - const descriptor: DraggableDescriptor = state.drag.initial.descriptor; - const home: ?DroppableDimension = state.dimension.droppable[descriptor.droppableId]; + const descriptor: DraggableDescriptor = appState.drag.initial.descriptor; + const home: ?DroppableDimension = appState.dimension.droppable[descriptor.droppableId]; if (!home) { return null; } - return getMemoizedDragStart( - descriptor.id, - descriptor.droppableId, - home.descriptor.type, - descriptor.index, - ); + const source: DraggableLocation = { + index: descriptor.index, + droppableId: descriptor.droppableId, + }; + + const start: DragStart = { + draggableId: descriptor.id, + type: home.descriptor.type, + source, + }; + + return start; }; - const onStateChange = (hooks: Hooks, previous: State, current: State): void => { + const finish = () => { + if (!state.isDragging) { + console.error('Drag finished but it had not started!'); + } + + setState(initial); + }; + + const onStateChange = (hooks: Hooks, previous: AppState, current: AppState): void => { const { onDragStart, onDragUpdate, onDragEnd } = hooks; const currentPhase = current.phase; const previousPhase = previous.phase; @@ -75,9 +100,6 @@ export default (announce: Announce): HookCaller => { // Dragging in progress if (currentPhase === 'DRAGGING' && previousPhase === 'DRAGGING') { // only call the onDragUpdate hook if something has changed from last time - if (!onDragUpdate) { - return; - } const start: ?DragStart = getDragStart(current); @@ -102,7 +124,36 @@ export default (announce: Announce): HookCaller => { destination, }; - onDragUpdate(result, announce); + // has not left the home position + if (!state.hasMovedFromStartLocation) { + // has not moved past the home yet + if (areLocationsEqual(start.source, destination)) { + return; + } + + setState({ + lastDestination: destination, + hasMovedFromStartLocation: true, + }); + + if (onDragUpdate) { + onDragUpdate(result, announce); + } + return; + } + + // has not moved from the previous location + if (areLocationsEqual(state.lastDestination, destination)) { + return; + } + + setState({ + lastUpdate: result, + }); + + if (onDragUpdate) { + onDragUpdate(result, announce); + } return; } @@ -114,11 +165,6 @@ export default (announce: Announce): HookCaller => { // Drag start if (currentPhase === 'DRAGGING' && previousPhase !== 'DRAGGING') { - // onDragStart is optional - if (!onDragStart) { - return; - } - const start: ?DragStart = getDragStart(current); if (!start) { @@ -126,14 +172,24 @@ export default (announce: Announce): HookCaller => { return; } + setState({ + isDragging: true, + hasMovedFromStartLocation: false, + start, + }); + + // onDragStart is optional + if (!onDragStart) { + return; + } + onDragStart(start, announce); return; } - - // Drag end if (currentPhase === 'DROP_COMPLETE' && previousPhase !== 'DROP_COMPLETE') { + finish(); if (!current.drop || !current.drop.result) { console.error('cannot fire onDragEnd hook without drag state', { current, previous }); return; @@ -174,6 +230,8 @@ export default (announce: Announce): HookCaller => { // Drag ended while dragging if (currentPhase === 'IDLE' && previousPhase === 'DRAGGING') { + finish(); + if (!previous.drag) { console.error('cannot fire onDragEnd for cancel because cannot find previous drag'); return; @@ -205,6 +263,7 @@ export default (announce: Announce): HookCaller => { // Drag ended during a drop animation. Not super sure how this can even happen. // This is being really safe if (currentPhase === 'IDLE' && previousPhase === 'DROP_ANIMATING') { + finish(); if (!previous.drop || !previous.drop.pending) { console.error('cannot fire onDragEnd for cancel because cannot find previous pending drop'); return; @@ -225,4 +284,5 @@ export default (announce: Announce): HookCaller => { }; return caller; -} +}; + diff --git a/test/unit/state/fire-hooks.spec.js b/test/unit/state/hook-caller.spec.js similarity index 80% rename from test/unit/state/fire-hooks.spec.js rename to test/unit/state/hook-caller.spec.js index e61a9963a8..cce6ff2745 100644 --- a/test/unit/state/fire-hooks.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -1,10 +1,11 @@ // @flow -import fireHooks from '../../../src/state/fire-hooks'; +import createHookCaller from '../../../src/state/hooks/hook-caller'; +import type { Hooks, HookCaller } from '../../../src/state/hooks/hooks-types'; import * as state from '../../utils/simple-state-preset'; import { getPreset } from '../../utils/dimension'; import type { + Announce, DropResult, - Hooks, State, DimensionState, DraggableLocation, @@ -21,8 +22,11 @@ const noDimensions: DimensionState = { describe('fire hooks', () => { let hooks: Hooks; + let caller: HookCaller; + const announceMock: Announce = () => { }; beforeEach(() => { + caller = createHookCaller(announceMock); hooks = { onDragStart: jest.fn(), onDragEnd: jest.fn(), @@ -36,7 +40,7 @@ describe('fire hooks', () => { describe('drag start', () => { it('should call the onDragStart hook when a drag starts', () => { - fireHooks(hooks, state.requesting(), state.dragging()); + caller.onStateChange(hooks, state.requesting(), state.dragging()); const expected: DragStart = { draggableId: preset.inHome1.descriptor.id, type: preset.home.descriptor.type, @@ -46,7 +50,7 @@ describe('fire hooks', () => { }, }; - expect(hooks.onDragStart).toHaveBeenCalledWith(expected); + expect(hooks.onDragStart).toHaveBeenCalledWith(expected, announceMock); }); it('should do nothing if no onDragStart is not provided', () => { @@ -54,7 +58,7 @@ describe('fire hooks', () => { onDragEnd: jest.fn(), }; - fireHooks(customHooks, state.requesting(), state.dragging()); + caller.onStateChange(customHooks, state.requesting(), state.dragging()); expect(console.error).not.toHaveBeenCalled(); }); @@ -65,14 +69,14 @@ describe('fire hooks', () => { drag: null, }; - fireHooks(hooks, state.requesting(), invalid); + caller.onStateChange(hooks, state.requesting(), invalid); expect(console.error).toHaveBeenCalled(); }); it('should not call if only collecting dimensions (not dragging yet)', () => { - fireHooks(hooks, state.idle, state.preparing); - fireHooks(hooks, state.preparing, state.requesting()); + caller.onStateChange(hooks, state.idle, state.preparing); + caller.onStateChange(hooks, state.preparing, state.requesting()); expect(hooks.onDragStart).not.toHaveBeenCalled(); }); @@ -110,14 +114,14 @@ describe('fire hooks', () => { dimension: noDimensions, }; - fireHooks(hooks, previous, current); + caller.onStateChange(hooks, previous, current); if (!current.drop || !current.drop.result) { throw new Error('invalid state'); } const provided: DropResult = current.drop.result; - expect(hooks.onDragEnd).toHaveBeenCalledWith(provided); + expect(hooks.onDragEnd).toHaveBeenCalledWith(provided, announceMock); }); it('should log an error and not call the callback if there is no drop result', () => { @@ -126,7 +130,7 @@ describe('fire hooks', () => { drop: null, }; - fireHooks(hooks, previous, invalid); + caller.onStateChange(hooks, previous, invalid); expect(hooks.onDragEnd).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalled(); @@ -152,9 +156,9 @@ describe('fire hooks', () => { dimension: noDimensions, }; - fireHooks(hooks, previous, current); + caller.onStateChange(hooks, previous, current); - expect(hooks.onDragEnd).toHaveBeenCalledWith(result); + expect(hooks.onDragEnd).toHaveBeenCalledWith(result, announceMock); }); it('should call onDragEnd with null if the item did not move', () => { @@ -185,9 +189,9 @@ describe('fire hooks', () => { destination: null, }; - fireHooks(hooks, previous, current); + caller.onStateChange(hooks, previous, current); - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected); + expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, announceMock); }); }); }); @@ -206,9 +210,9 @@ describe('fire hooks', () => { destination: null, }; - fireHooks(hooks, state.dragging(), state.idle); + caller.onStateChange(hooks, state.dragging(), state.idle); - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected); + expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, announceMock); }); it('should log an error and do nothing if it cannot find a previous drag to publish', () => { @@ -219,7 +223,7 @@ describe('fire hooks', () => { dimension: noDimensions, }; - fireHooks(hooks, state.idle, invalid); + caller.onStateChange(hooks, state.idle, invalid); expect(hooks.onDragEnd).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalled(); @@ -239,9 +243,9 @@ describe('fire hooks', () => { destination: null, }; - fireHooks(hooks, state.dropAnimating(), state.idle); + caller.onStateChange(hooks, state.dropAnimating(), state.idle); - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected); + expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, announceMock); }); it('should log an error and do nothing if it cannot find a previous drag to publish', () => { @@ -250,7 +254,7 @@ describe('fire hooks', () => { drop: null, }; - fireHooks(hooks, invalid, state.idle); + caller.onStateChange(hooks, invalid, state.idle); expect(hooks.onDragEnd).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalled(); @@ -263,7 +267,7 @@ describe('fire hooks', () => { Object.keys(state).forEach((key: string) => { const current: State = state[key]; - fireHooks(hooks, current, current); + caller.onStateChange(hooks, current, current); expect(hooks.onDragStart).not.toHaveBeenCalled(); expect(hooks.onDragEnd).not.toHaveBeenCalled(); From 26b3ce45a6d66e1beb221b3da49771e7ea5ce6dd Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 12:01:44 +1100 Subject: [PATCH 082/163] adding tests --- test/unit/state/hook-caller.spec.js | 286 +++++++++++++++++++++++++++- test/utils/simple-state-preset.js | 2 +- 2 files changed, 286 insertions(+), 2 deletions(-) diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index cce6ff2745..132dc6cf0d 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -3,6 +3,7 @@ import createHookCaller from '../../../src/state/hooks/hook-caller'; import type { Hooks, HookCaller } from '../../../src/state/hooks/hooks-types'; import * as state from '../../utils/simple-state-preset'; import { getPreset } from '../../utils/dimension'; +import noImpact, { noMovement } from '../../../src/state/no-impact'; import type { Announce, DropResult, @@ -10,6 +11,7 @@ import type { DimensionState, DraggableLocation, DragStart, + DragImpact, } from '../../../src/types'; const preset = getPreset(); @@ -29,6 +31,7 @@ describe('fire hooks', () => { caller = createHookCaller(announceMock); hooks = { onDragStart: jest.fn(), + onDragUpdate: jest.fn(), onDragEnd: jest.fn(), }; jest.spyOn(console, 'error').mockImplementation(() => { }); @@ -40,7 +43,6 @@ describe('fire hooks', () => { describe('drag start', () => { it('should call the onDragStart hook when a drag starts', () => { - caller.onStateChange(hooks, state.requesting(), state.dragging()); const expected: DragStart = { draggableId: preset.inHome1.descriptor.id, type: preset.home.descriptor.type, @@ -50,6 +52,8 @@ describe('fire hooks', () => { }, }; + caller.onStateChange(hooks, state.requesting(), state.dragging()); + expect(hooks.onDragStart).toHaveBeenCalledWith(expected, announceMock); }); @@ -82,6 +86,286 @@ describe('fire hooks', () => { }); }); + describe('drag update', () => { + const withImpact = (current: State, impact: DragImpact) => { + if (!current.drag) { + throw new Error('invalid state'); + } + return { + ...current, + drag: { + ...current.drag, + impact, + }, + }; + }; + + const start: DragStart = { + draggableId: preset.inHome1.descriptor.id, + type: preset.home.descriptor.type, + source: { + index: preset.inHome1.descriptor.index, + droppableId: preset.inHome1.descriptor.droppableId, + }, + }; + + const inHomeImpact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + destination: start.source, + }; + + describe('has not moved from home location', () => { + it('should not provide an update if the location has not changed since the last drag', () => { + // start a drag + caller.onStateChange(hooks, state.requesting(), state.dragging()); + expect(hooks.onDragUpdate).not.toHaveBeenCalled(); + + // drag to the same spot + caller.onStateChange( + hooks, + withImpact(state.dragging(), inHomeImpact), + withImpact(state.dragging(), inHomeImpact), + ); + + expect(hooks.onDragUpdate).not.toHaveBeenCalled(); + }); + + it('should provide an update if the index changes', () => { + const destination: DraggableLocation = { + index: preset.inHome1.descriptor.index + 1, + droppableId: preset.inHome1.descriptor.droppableId, + }; + const impact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + destination, + }; + const expected: DropResult = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination, + }; + + // drag to the same spot + caller.onStateChange( + hooks, + withImpact(state.dragging(), inHomeImpact), + withImpact(state.dragging(), impact), + ); + + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + }); + + it('should provide an update if the droppable changes', () => { + const destination: DraggableLocation = { + // same index + index: preset.inHome1.descriptor.index, + // different droppable + droppableId: preset.foreign.descriptor.id, + }; + const impact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + destination, + }; + const expected: DropResult = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination, + }; + + // drag to the same spot + caller.onStateChange( + hooks, + withImpact(state.dragging(), inHomeImpact), + withImpact(state.dragging(), impact), + ); + + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + }); + + it('should provide an update if moving from a droppable to nothing', () => { + const expected: DropResult = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination: null, + }; + + // drag to the same spot + caller.onStateChange( + hooks, + withImpact(state.dragging(), inHomeImpact), + withImpact(state.dragging(), noImpact), + ); + + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + }); + }); + + describe('no longer in home location', () => { + const firstImpact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + // moved into the second index + destination: { + index: preset.inHome1.descriptor.index + 1, + droppableId: preset.inHome1.descriptor.droppableId, + }, + }; + + beforeEach(() => { + // initial lift + caller.onStateChange( + hooks, + state.requesting(), + withImpact(state.dragging(), inHomeImpact), + ); + // checking everything is well + expect(hooks.onDragStart).toHaveBeenCalled(); + expect(hooks.onDragUpdate).not.toHaveBeenCalled(); + + // first move into new location + caller.onStateChange( + hooks, + withImpact(state.dragging(), inHomeImpact), + withImpact(state.dragging(), firstImpact), + ); + + expect(hooks.onDragUpdate).toHaveBeenCalled(); + hooks.onDragUpdate.mockReset(); + }); + + it('should not provide an update if the location has not changed since the last drag', () => { + // drag to the same spot + caller.onStateChange( + hooks, + withImpact(state.dragging(), firstImpact), + withImpact(state.dragging(), firstImpact), + ); + + expect(hooks.onDragUpdate).not.toHaveBeenCalled(); + }); + + it('should provide an update if the index changes', () => { + const destination: DraggableLocation = { + index: preset.inHome1.descriptor.index + 2, + droppableId: preset.inHome1.descriptor.droppableId, + }; + const secondImpact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + destination, + }; + const expected: DropResult = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination, + }; + + // drag to the same spot + caller.onStateChange( + hooks, + withImpact(state.dragging(), firstImpact), + withImpact(state.dragging(), secondImpact), + ); + + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + }); + + it('should provide an update if the droppable changes', () => { + const destination: DraggableLocation = { + index: preset.inHome1.descriptor.index + 1, + droppableId: preset.foreign.descriptor.id, + }; + const secondImpact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + destination, + }; + const expected: DropResult = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination, + }; + + // drag to the same spot + caller.onStateChange( + hooks, + withImpact(state.dragging(), firstImpact), + withImpact(state.dragging(), secondImpact), + ); + + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + }); + + it('should provide an update if moving from a droppable to nothing', () => { + const secondImpact: DragImpact = { + movement: noMovement, + direction: null, + destination: null, + }; + const expected: DropResult = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination: null, + }; + + // drag to the same spot + caller.onStateChange( + hooks, + withImpact(state.dragging(), firstImpact), + withImpact(state.dragging(), secondImpact), + ); + + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + }); + + it('should provide an update if moving back to the home location', () => { + const impact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + destination: null, + }; + + // drag to nowhere + caller.onStateChange( + hooks, + withImpact(state.dragging(), inHomeImpact), + withImpact(state.dragging(), impact), + ); + const first: DropResult = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination: null, + }; + + expect(hooks.onDragUpdate).toHaveBeenCalledWith(first, announceMock); + + // drag back to home + caller.onStateChange( + hooks, + withImpact(state.dragging(), impact), + withImpact(state.dragging(), inHomeImpact), + ); + const second: DropResult = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination: start.source, + }; + expect(hooks.onDragUpdate).toHaveBeenCalledWith(second, announceMock); + }); + }); + }); + describe('drag end', () => { // it is possible to complete a drag from a DRAGGING or DROP_ANIMATING (drop or cancel) const preEndStates: State[] = [ diff --git a/test/utils/simple-state-preset.js b/test/utils/simple-state-preset.js index bbfb0f8443..4c76996b6f 100644 --- a/test/utils/simple-state-preset.js +++ b/test/utils/simple-state-preset.js @@ -165,7 +165,7 @@ export const scrollJumpRequest = (request: Position): State => { }; return result; -} +}; const getDropAnimating = (id: DraggableId, trigger: DropTrigger): State => { const descriptor: DraggableDescriptor = preset.draggables[id].descriptor; From 27224f45f8f3ab1e7dffc6c819e08459f208648e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 12:23:45 +1100 Subject: [PATCH 083/163] more tests for hooks --- src/state/hooks/hook-caller.js | 22 +-- .../drag-drop-context/drag-drop-context.jsx | 4 +- stories/src/vertical/quote-app.jsx | 4 +- test/unit/state/hook-caller.spec.js | 134 ++++++++++++++++++ 4 files changed, 144 insertions(+), 20 deletions(-) diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index 98f9e0b7f5..6cf245a0df 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -6,9 +6,6 @@ import type { DragState, DragStart, DropResult, - DraggableId, - DroppableId, - TypeId, DraggableLocation, DraggableDescriptor, DroppableDimension, @@ -84,14 +81,6 @@ export default (announce: Announce): HookCaller => { return start; }; - const finish = () => { - if (!state.isDragging) { - console.error('Drag finished but it had not started!'); - } - - setState(initial); - }; - const onStateChange = (hooks: Hooks, previous: AppState, current: AppState): void => { const { onDragStart, onDragUpdate, onDragEnd } = hooks; const currentPhase = current.phase; @@ -148,7 +137,7 @@ export default (announce: Announce): HookCaller => { } setState({ - lastUpdate: result, + lastDestination: destination, }); if (onDragUpdate) { @@ -157,6 +146,11 @@ export default (announce: Announce): HookCaller => { return; } + // We are not in the dragging phase so we can clear this state + if (state.isDragging) { + setState(initial); + } + // From this point we only care about phase changes if (currentPhase === previousPhase) { @@ -189,7 +183,6 @@ export default (announce: Announce): HookCaller => { // Drag end if (currentPhase === 'DROP_COMPLETE' && previousPhase !== 'DROP_COMPLETE') { - finish(); if (!current.drop || !current.drop.result) { console.error('cannot fire onDragEnd hook without drag state', { current, previous }); return; @@ -230,8 +223,6 @@ export default (announce: Announce): HookCaller => { // Drag ended while dragging if (currentPhase === 'IDLE' && previousPhase === 'DRAGGING') { - finish(); - if (!previous.drag) { console.error('cannot fire onDragEnd for cancel because cannot find previous drag'); return; @@ -263,7 +254,6 @@ export default (announce: Announce): HookCaller => { // Drag ended during a drop animation. Not super sure how this can even happen. // This is being really safe if (currentPhase === 'IDLE' && previousPhase === 'DROP_ANIMATING') { - finish(); if (!previous.drop || !previous.drop.pending) { console.error('cannot fire onDragEnd for cancel because cannot find previous pending drop'); return; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index f575863caa..f05e73210a 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -150,7 +150,7 @@ export default class DragDropContext extends React.Component { onDragEnd: this.props.onDragEnd, onDragUpdate: this.props.onDragUpdate, }; - this.hookCaller.onStateChange(hooks, previous, current); + this.hookCaller.onStateChange(hooks, previousValue, current); if (current.phase !== previousValue.phase) { // executing phase change handlers first @@ -162,7 +162,7 @@ export default class DragDropContext extends React.Component { this.dimensionMarshal.onPhaseChange(current); } - this.autoScroller.onStateChange(previous, current); + this.autoScroller.onStateChange(previousValue, current); }); } diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index e2fa5220a7..6d6cfb5390 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -41,7 +41,7 @@ export default class QuoteApp extends Component { }; onDragStart = (initial: DragStart, announce: Announce) => { - announce('drag start!'); + announce(`drag start: item ${initial.draggableId} lifted in pos ${initial.source.index}`); console.log('initial', initial); publishOnDragStart(initial); // Add a little vibration if the browser supports it. @@ -52,7 +52,7 @@ export default class QuoteApp extends Component { } onDragUpdate = (current: DropResult, announce: Announce) => { - announce('update!'); + announce(`drag start: item ${current.draggableId} moved to pos ${current.destination ? current.destination.index : 'nowhere'}`); console.log('current', current); } diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index 132dc6cf0d..9e8e589a72 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -236,6 +236,7 @@ describe('fire hooks', () => { ); expect(hooks.onDragUpdate).toHaveBeenCalled(); + // $ExpectError - no mock reset property hooks.onDragUpdate.mockReset(); }); @@ -364,6 +365,139 @@ describe('fire hooks', () => { expect(hooks.onDragUpdate).toHaveBeenCalledWith(second, announceMock); }); }); + + describe('multiple updates', () => { + it('should correctly update across multiple updates', () => { + // initial lift + caller.onStateChange( + hooks, + state.requesting(), + withImpact(state.dragging(), inHomeImpact), + ); + // checking everything is well + expect(hooks.onDragStart).toHaveBeenCalled(); + expect(hooks.onDragUpdate).not.toHaveBeenCalled(); + + // first move into new location + const firstImpact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + // moved into the second index + destination: { + index: preset.inHome1.descriptor.index + 1, + droppableId: preset.inHome1.descriptor.droppableId, + }, + }; + caller.onStateChange( + hooks, + withImpact(state.dragging(), inHomeImpact), + withImpact(state.dragging(), firstImpact), + ); + + expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); + expect(hooks.onDragUpdate).toHaveBeenCalledWith({ + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination: firstImpact.destination, + }, announceMock); + + // second move into new location + const secondImpact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + // moved into the second index + destination: { + index: preset.inHome1.descriptor.index + 2, + droppableId: preset.inHome1.descriptor.droppableId, + }, + }; + caller.onStateChange( + hooks, + withImpact(state.dragging(), firstImpact), + withImpact(state.dragging(), secondImpact), + ); + + expect(hooks.onDragUpdate).toHaveBeenCalledTimes(2); + expect(hooks.onDragUpdate).toHaveBeenCalledWith({ + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination: secondImpact.destination, + }, announceMock); + }); + + it('should update correctly across multiple drags', () => { + // initial lift + caller.onStateChange( + hooks, + state.requesting(), + withImpact(state.dragging(), inHomeImpact), + ); + // checking everything is well + expect(hooks.onDragStart).toHaveBeenCalled(); + expect(hooks.onDragUpdate).not.toHaveBeenCalled(); + + // first move into new location + const firstImpact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + // moved into the second index + destination: { + index: preset.inHome1.descriptor.index + 1, + droppableId: preset.inHome1.descriptor.droppableId, + }, + }; + caller.onStateChange( + hooks, + withImpact(state.dragging(), inHomeImpact), + withImpact(state.dragging(), firstImpact), + ); + expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); + expect(hooks.onDragUpdate).toHaveBeenCalledWith({ + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination: firstImpact.destination, + }, announceMock); + // resetting the mock + // $ExpectError - resetting mock + hooks.onDragUpdate.mockReset(); + + // drop + caller.onStateChange( + hooks, + withImpact(state.dragging(), firstImpact), + state.idle, + ); + + expect(hooks.onDragUpdate).not.toHaveBeenCalled(); + + // a new lift! + caller.onStateChange( + hooks, + state.requesting(), + withImpact(state.dragging(), inHomeImpact), + ); + // checking everything is well + expect(hooks.onDragStart).toHaveBeenCalled(); + expect(hooks.onDragUpdate).not.toHaveBeenCalled(); + + // first move into new location + caller.onStateChange( + hooks, + withImpact(state.dragging(), inHomeImpact), + withImpact(state.dragging(), firstImpact), + ); + expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); + expect(hooks.onDragUpdate).toHaveBeenCalledWith({ + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination: firstImpact.destination, + }, announceMock); + }); + }); }); describe('drag end', () => { From d556f87557673682903bb66868a6a06f5fbf6b13 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 13:12:20 +1100 Subject: [PATCH 084/163] minor improvements --- src/state/hooks/hook-caller.js | 3 --- src/state/reducer.js | 2 ++ stories/src/vertical/quote-app.jsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index 6cf245a0df..60b488a40a 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -11,9 +11,6 @@ import type { DroppableDimension, } from '../../types'; -// const announce: Announce = (message: string) => -// console.log(`%c ${message}`, 'color: green; font-size: 20px;'); - type State = { isDragging: boolean, start: ?DraggableLocation, diff --git a/src/state/reducer.js b/src/state/reducer.js index 56cf3181b4..2b7e340658 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -402,6 +402,8 @@ export default (state: State = clean('IDLE'), action: Action): State => { } if (action.type === 'MOVE') { + // TODO: finished initial collection? + // Otherwise get an incorrect index calculated before the other dimensions are published const { client, windowScroll, shouldAnimate } = action.payload; return move({ state, diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index 6d6cfb5390..9958020f2e 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -52,7 +52,7 @@ export default class QuoteApp extends Component { } onDragUpdate = (current: DropResult, announce: Announce) => { - announce(`drag start: item ${current.draggableId} moved to pos ${current.destination ? current.destination.index : 'nowhere'}`); + announce(`update: item ${current.draggableId} moved to pos ${current.destination ? current.destination.index : 'nowhere'}`); console.log('current', current); } From c11e3f5e5af328dd54a74d6f9f0e2a809af6bb43 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 14:48:38 +1100 Subject: [PATCH 085/163] goodbye aria-grabbed --- src/state/hooks/hook-caller.js | 4 + src/view/announcer/announcer-types.js | 9 ++ src/view/announcer/announcer.js | 98 +++++++++++++++++++ .../drag-drop-context/drag-drop-context.jsx | 11 ++- src/view/drag-handle/drag-handle-types.js | 3 - src/view/drag-handle/drag-handle.jsx | 8 +- stories/1-single-vertical-list-story.js | 3 +- stories/src/vertical/quote-app.jsx | 3 - 8 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 src/view/announcer/announcer-types.js create mode 100644 src/view/announcer/announcer.js diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index 60b488a40a..6713177656 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -85,6 +85,10 @@ export default (announce: Announce): HookCaller => { // Dragging in progress if (currentPhase === 'DRAGGING' && previousPhase === 'DRAGGING') { + if (!state.isDragging) { + console.error('Cannot process dragging update if drag has not started'); + return; + } // only call the onDragUpdate hook if something has changed from last time const start: ?DragStart = getDragStart(current); diff --git a/src/view/announcer/announcer-types.js b/src/view/announcer/announcer-types.js new file mode 100644 index 0000000000..aaebd9f61f --- /dev/null +++ b/src/view/announcer/announcer-types.js @@ -0,0 +1,9 @@ +// @flow +import type { Announce } from '../../types'; + +export type Announcer = {| + announce: Announce, + describedBy: string, + mount: () => void, + unmount: () => void, +|} diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js new file mode 100644 index 0000000000..88f8c3656b --- /dev/null +++ b/src/view/announcer/announcer.js @@ -0,0 +1,98 @@ +// @flow +import type { Announce } from '../../types'; +import type { Announcer } from './announcer-types'; + +type State = {| + el: ?HTMLElement, +|} + +let count: number = 0; + +export default (): Announcer => { + const id: string = `data-react-beautiful-dnd-announcement-${count++}`; + + let state: State = { + el: null, + }; + + const setState = (newState: State) => { + state = newState; + }; + + const announce: Announce = (message: string): void => { + if (!state.el) { + console.error('Cannot announce to unmounted node'); + return; + } + + state.el.textContent = message; + console.log(`%c ${message}`, 'color: green; font-size: 20px;'); + console.log(state.el); + }; + + const mount = () => { + if (state.el) { + console.error('Announcer already mounted'); + return; + } + + const el: HTMLElement = document.createElement('div'); + // identifier + el.id = id; + + // Aria live region + + // will force itself to be read + el.setAttribute('aria-live', 'assertive'); + el.setAttribute('role', 'log'); + // must read the whole thing every time + el.setAttribute('aria-atomic', 'true'); + + // style + el.style.position = 'absolute'; + el.style.top = '0px'; + el.style.fontSize = '30px'; + el.style.backgroundColor = 'rgba(255,255,255,0.4)'; + + // aria + + if (!document.body) { + throw new Error('Cannot find the head to append a style to'); + } + + // add el tag to body + document.body.appendChild(el); + setState({ + el, + }); + }; + + const unmount = () => { + if (!state.el) { + console.error('Will not unmount annoucer as it is already unmounted'); + return; + } + const previous = state.el; + + setState({ + el: null, + }); + + if (!previous.parentNode) { + console.error('Cannot unmount style marshal as cannot find parent'); + return; + } + + previous.parentNode.removeChild(previous); + }; + + const announcer: Announcer = { + announce, + describedBy: id, + mount, + unmount, + }; + + return announcer; +}; + diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index f05e73210a..b42ee51937 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -7,6 +7,8 @@ import createDimensionMarshal from '../../state/dimension-marshal/dimension-mars import createStyleMarshal from '../style-marshal/style-marshal'; import canStartDrag from '../../state/can-start-drag'; import scrollWindow from '../../window/scroll-window'; +import createAnnouncer from '../announcer/announcer'; +import type { Announcer } from '../announcer/announcer-types'; import createAutoScroller from '../../state/auto-scroller'; import type { AutoScroller } from '../../state/auto-scroller/auto-scroller-types'; import type { StyleMarshal } from '../style-marshal/style-marshal-types'; @@ -58,6 +60,7 @@ export default class DragDropContext extends React.Component { styleMarshal: StyleMarshal autoScroller: AutoScroller hookCaller: HookCaller + announcer: Announcer unsubscribe: Function // Need to declare childContextTypes without flow @@ -95,10 +98,10 @@ export default class DragDropContext extends React.Component { componentWillMount() { this.store = createStore(); + this.announcer = createAnnouncer(); + // create the hook caller - this.hookCaller = createHookCaller( - (message: string) => console.log(`%c ${message}`, 'color: green; font-size: 20px;') - ); + this.hookCaller = createHookCaller(this.announcer.announce); // create the style marshal this.styleMarshal = createStyleMarshal(); @@ -171,11 +174,13 @@ export default class DragDropContext extends React.Component { // this cannot be done before otherwise it would break // server side rendering this.styleMarshal.mount(); + this.announcer.mount(); } componentWillUnmount() { this.unsubscribe(); this.styleMarshal.unmount(); + this.announcer.unmount(); } render() { diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/drag-handle/drag-handle-types.js index 4e7f400390..556a79cf51 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/drag-handle/drag-handle-types.js @@ -34,9 +34,6 @@ export type DragHandleProps = {| // Allow tabbing to this element tabIndex: number, - // Aria - 'aria-grabbed': boolean, - // Stop html5 drag and drop draggable: boolean, onDragStart: () => boolean, diff --git a/src/view/drag-handle/drag-handle.jsx b/src/view/drag-handle/drag-handle.jsx index 91872ea9a2..7eea5b3155 100644 --- a/src/view/drag-handle/drag-handle.jsx +++ b/src/view/drag-handle/drag-handle.jsx @@ -176,13 +176,10 @@ export default class DragHandle extends Component { return shouldAllowDraggingFromTarget(event, this.props); } - isAnySensorDragging = (): boolean => - this.sensors.some((sensor: Sensor) => sensor.isDragging()) - isAnySensorCapturing = (): boolean => this.sensors.some((sensor: Sensor) => sensor.isCapturing()) - getProvided = memoizeOne((isEnabled: boolean, isDragging: boolean): ?DragHandleProps => { + getProvided = memoizeOne((isEnabled: boolean): ?DragHandleProps => { if (!isEnabled) { return null; } @@ -194,7 +191,6 @@ export default class DragHandle extends Component { onTouchMove: this.onTouchMove, onClick: this.onClick, tabIndex: 0, - 'aria-grabbed': isDragging, 'data-react-beautiful-dnd-drag-handle': this.styleContext, draggable: false, onDragStart: getFalse, @@ -207,6 +203,6 @@ export default class DragHandle extends Component { render() { const { children, isEnabled } = this.props; - return children(this.getProvided(isEnabled, this.isAnySensorDragging())); + return children(this.getProvided(isEnabled)); } } diff --git a/stories/1-single-vertical-list-story.js b/stories/1-single-vertical-list-story.js index 2039a37742..bc4a0281f2 100644 --- a/stories/1-single-vertical-list-story.js +++ b/stories/1-single-vertical-list-story.js @@ -7,7 +7,8 @@ import { quotes, getQuotes } from './src/data'; import { grid } from './src/constants'; const data = { - small: quotes, + // small: quotes, + small: getQuotes(3), medium: getQuotes(40), large: getQuotes(500), }; diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index 9958020f2e..964c8ba98e 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -42,7 +42,6 @@ export default class QuoteApp extends Component { onDragStart = (initial: DragStart, announce: Announce) => { announce(`drag start: item ${initial.draggableId} lifted in pos ${initial.source.index}`); - console.log('initial', initial); publishOnDragStart(initial); // Add a little vibration if the browser supports it. // Add's a nice little physical feedback @@ -53,12 +52,10 @@ export default class QuoteApp extends Component { onDragUpdate = (current: DropResult, announce: Announce) => { announce(`update: item ${current.draggableId} moved to pos ${current.destination ? current.destination.index : 'nowhere'}`); - console.log('current', current); } onDragEnd = (result: DropResult, announce: Announce) => { announce('on drop!'); - console.log('result', result); publishOnDragEnd(result); // dropped outside the list From 561a363e39edc355350d10081d14293f94f5846b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 16:17:07 +1100 Subject: [PATCH 086/163] changing shape --- src/state/action-creators.js | 9 +--- src/state/can-start-drag.js | 2 +- src/state/hooks/hook-caller.js | 44 ++++--------------- src/state/hooks/hooks-types.js | 10 +---- src/state/reducer.js | 3 +- src/types.js | 21 ++++++--- .../drag-drop-context/drag-drop-context.jsx | 2 +- src/view/style-marshal/style-marshal.js | 6 +-- stories/src/accessible/task-list.jsx | 2 + stories/src/accessible/task.jsx | 20 +++++++++ stories/src/accessible/types.js | 6 +++ stories/src/primatives/quote-item.jsx | 4 +- stories/src/vertical/quote-app.jsx | 22 +++++++--- 13 files changed, 81 insertions(+), 70 deletions(-) create mode 100644 stories/src/accessible/task-list.jsx create mode 100644 stories/src/accessible/task.jsx create mode 100644 stories/src/accessible/types.js diff --git a/src/state/action-creators.js b/src/state/action-creators.js index d073e0a0ae..11a86337dd 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -11,7 +11,6 @@ import type { Position, Dispatch, State, - DropTrigger, CurrentDrag, InitialDrag, DraggableDescriptor, @@ -242,7 +241,6 @@ export const prepare = (): PrepareAction => ({ export type DropAnimateAction = { type: 'DROP_ANIMATE', payload: {| - trigger: DropTrigger, newHomeOffset: Position, impact: DragImpact, result: DropResult, @@ -250,21 +248,18 @@ export type DropAnimateAction = { } type AnimateDropArgs = {| - trigger: DropTrigger, newHomeOffset: Position, impact: DragImpact, result: DropResult |} const animateDrop = ({ - trigger, newHomeOffset, impact, result, }: AnimateDropArgs): DropAnimateAction => ({ type: 'DROP_ANIMATE', payload: { - trigger, newHomeOffset, impact, result, @@ -322,6 +317,7 @@ export const drop = () => type: home.descriptor.type, source, destination: impact.destination, + reason: 'DROP', }; const newCenter: Position = getNewHomeClientCenter({ @@ -353,7 +349,6 @@ export const drop = () => } dispatch(animateDrop({ - trigger: 'DROP', newHomeOffset, impact, result, @@ -391,6 +386,7 @@ export const cancel = () => source, // no destination when cancelling destination: null, + reason: 'CANCEL', }; const isAnimationRequired = !isEqual(current.client.offset, origin); @@ -403,7 +399,6 @@ export const cancel = () => const scrollDiff: Position = getScrollDiff({ initial, current, droppable: home }); dispatch(animateDrop({ - trigger: 'CANCEL', newHomeOffset: scrollDiff, impact: noImpact, result, diff --git a/src/state/can-start-drag.js b/src/state/can-start-drag.js index f761d82d6d..d67aafd27f 100644 --- a/src/state/can-start-drag.js +++ b/src/state/can-start-drag.js @@ -38,7 +38,7 @@ export default (state: State, id: DraggableId): boolean => { // if dropping - allow lifting // if cancelling - disallow lifting - return state.drop.pending.trigger === 'DROP'; + return state.drop.pending.result.reason === 'DROP'; } // this should not happen diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index 6713177656..46aa47d3f8 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -1,10 +1,12 @@ // @flow -import type { Hooks, HookCaller } from './hooks-types'; +import type { HookCaller } from './hooks-types'; import type { Announce, + Hooks, State as AppState, DragState, DragStart, + DragUpdate, DropResult, DraggableLocation, DraggableDescriptor, @@ -107,7 +109,7 @@ export default (announce: Announce): HookCaller => { const destination: ?DraggableLocation = drag.impact.destination; - const result: DropResult = { + const update: DragUpdate = { draggableId: start.draggableId, type: start.type, source: start.source, @@ -127,7 +129,7 @@ export default (announce: Announce): HookCaller => { }); if (onDragUpdate) { - onDragUpdate(result, announce); + onDragUpdate(update, announce); } return; } @@ -142,7 +144,7 @@ export default (announce: Announce): HookCaller => { }); if (onDragUpdate) { - onDragUpdate(result, announce); + onDragUpdate(update, announce); } return; } @@ -189,37 +191,7 @@ export default (announce: Announce): HookCaller => { return; } - const { - source, - destination, - draggableId, - type, - } = current.drop.result; - - // Could be a cancel or a drop nowhere - if (!destination) { - onDragEnd(current.drop.result, announce); - return; - } - - // Do not publish a result.destination where nothing moved - const didMove: boolean = source.droppableId !== destination.droppableId || - source.index !== destination.index; - - if (didMove) { - onDragEnd(current.drop.result, announce); - return; - } - - const muted: DropResult = { - draggableId, - type, - source, - destination: null, - }; - - onDragEnd(muted, announce); - return; + onDragEnd(current.drop.result, announce); } // Drag ended while dragging @@ -247,6 +219,7 @@ export default (announce: Announce): HookCaller => { type: home.descriptor.type, source, destination: null, + reason: 'CANCEL', }; onDragEnd(result, announce); return; @@ -265,6 +238,7 @@ export default (announce: Announce): HookCaller => { type: previous.drop.pending.result.type, source: previous.drop.pending.result.source, destination: null, + reason: 'CANCEL', }; onDragEnd(result, announce); } diff --git a/src/state/hooks/hooks-types.js b/src/state/hooks/hooks-types.js index 31fd5d4099..6854aeacdc 100644 --- a/src/state/hooks/hooks-types.js +++ b/src/state/hooks/hooks-types.js @@ -1,17 +1,9 @@ // @flow import type { - DragStart, - DropResult, - Announce, State, + Hooks, } from '../../types'; -export type Hooks = {| - onDragStart?: (start: DragStart, announce: Announce) => void, - onDragUpdate?: (current: DropResult, announce: Announce) => void, - onDragEnd: (result: DropResult, announce: Announce) => void, -|} - export type HookCaller = {| onStateChange: (hooks: Hooks, previous: State, current: State) => void, |} diff --git a/src/state/reducer.js b/src/state/reducer.js index 2b7e340658..2e319cf0d5 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -538,7 +538,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { } if (action.type === 'DROP_ANIMATE') { - const { trigger, newHomeOffset, impact, result } = action.payload; + const { newHomeOffset, impact, result } = action.payload; if (state.phase !== 'DRAGGING') { console.error('cannot animate drop while not dragging', action); @@ -551,7 +551,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { } const pending: PendingDrop = { - trigger, newHomeOffset, result, impact, diff --git a/src/types.js b/src/types.js index 2c5f821b16..aadb15b8fa 100644 --- a/src/types.js +++ b/src/types.js @@ -231,13 +231,20 @@ export type DragStart = {| source: DraggableLocation, |} -// published when a drag finishes -export type DropResult = {| +export type DragUpdate = {| ...DragStart, // may not have any destination (drag to nowhere) destination: ?DraggableLocation, |} +export type DropReason = 'DROP' | 'CANCEL'; + +// published when a drag finishes +export type DropResult = {| + ...DragUpdate, + reason: DropReason, +|} + export type DragState = {| initial: InitialDrag, current: CurrentDrag, @@ -246,10 +253,7 @@ export type DragState = {| scrollJumpRequest: ?Position, |} -export type DropTrigger = 'DROP' | 'CANCEL'; - export type PendingDrop = {| - trigger: DropTrigger, newHomeOffset: Position, impact: DragImpact, result: DropResult, @@ -311,3 +315,10 @@ export type Dispatch = ReduxDispatch; export type Store = ReduxStore; export type Announce = (message: string) => void; + +export type Hooks = {| + onDragStart?: (start: DragStart, announce: Announce) => void, + onDragUpdate?: (update: DragUpdate, announce: Announce) => void, + onDragEnd: (result: DropResult, announce: Announce) => void, +|} + diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index b42ee51937..714b641823 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -24,10 +24,10 @@ import type { DroppableDimension, DroppableId, Position, + Hooks, } from '../../types'; import type { HookCaller, - Hooks, } from '../../state/hooks/hooks-types'; import { storeKey, diff --git a/src/view/style-marshal/style-marshal.js b/src/view/style-marshal/style-marshal.js index f048e67222..06096350c8 100644 --- a/src/view/style-marshal/style-marshal.js +++ b/src/view/style-marshal/style-marshal.js @@ -5,7 +5,7 @@ import type { StyleMarshal } from './style-marshal-types'; import type { State as AppState, Phase, - DropTrigger, + DropReason, } from '../../types'; let count: number = 0; @@ -89,9 +89,9 @@ export default () => { return; } - const trigger: DropTrigger = current.drop.pending.trigger; + const reason: DropReason = current.drop.pending.result.reason; - if (trigger === 'DROP') { + if (reason === 'DROP') { setStyle(styles.dropAnimating); return; } diff --git a/stories/src/accessible/task-list.jsx b/stories/src/accessible/task-list.jsx new file mode 100644 index 0000000000..ee165f4089 --- /dev/null +++ b/stories/src/accessible/task-list.jsx @@ -0,0 +1,2 @@ +// @flow + diff --git a/stories/src/accessible/task.jsx b/stories/src/accessible/task.jsx new file mode 100644 index 0000000000..68bd1ff213 --- /dev/null +++ b/stories/src/accessible/task.jsx @@ -0,0 +1,20 @@ +// @flow +import React, { Component } from 'react'; +import styled from 'styled-components'; +import type { Task } from './types'; + +type Props = {| + task: Task +|} + +const Container = styled.div` + background: lightblue; +`; + +export class Task extends Component { + render() { + return ( + {this.props.task.content} + ); + } +} diff --git a/stories/src/accessible/types.js b/stories/src/accessible/types.js new file mode 100644 index 0000000000..754a20f2df --- /dev/null +++ b/stories/src/accessible/types.js @@ -0,0 +1,6 @@ +// @flow + +export type Task = {| + id: string, + content: string, +|} diff --git a/stories/src/primatives/quote-item.jsx b/stories/src/primatives/quote-item.jsx index 71a268f6c9..c5d6d358e6 100644 --- a/stories/src/primatives/quote-item.jsx +++ b/stories/src/primatives/quote-item.jsx @@ -15,7 +15,7 @@ type Props = { type HTMLElement = any; -const Container = styled.a` +const Container = styled.div` border-radius: ${borderRadius}px; border: 1px solid grey; background-color: ${({ isDragging }) => (isDragging ? colors.green : colors.white)}; @@ -116,7 +116,7 @@ export default class QuoteItem extends React.PureComponent { return ( { }; onDragStart = (initial: DragStart, announce: Announce) => { - announce(`drag start: item ${initial.draggableId} lifted in pos ${initial.source.index}`); + announce(` + You have started dragging the quote in position ${initial.source.index + 1} of ${this.state.quotes.length}. + You can use your arrow keys to move the quote around, space bar to drop, and escape to cancel. + `); publishOnDragStart(initial); // Add a little vibration if the browser supports it. // Add's a nice little physical feedback @@ -50,12 +53,21 @@ export default class QuoteApp extends Component { } } - onDragUpdate = (current: DropResult, announce: Announce) => { - announce(`update: item ${current.draggableId} moved to pos ${current.destination ? current.destination.index : 'nowhere'}`); + onDragUpdate = (update: DragUpdate, announce: Announce) => { + if (!update.destination) { + announce('You are currently not dragging over any droppable area'); + return; + } + announce(`You have moved the quote into position ${update.destination.index + 1} of ${this.state.quotes.length}`); } onDragEnd = (result: DropResult, announce: Announce) => { - announce('on drop!'); + if (result.reason === 'CANCEL') { + announce('drop cancelled'); + } else { + announce('drop success'); + } + publishOnDragEnd(result); // dropped outside the list From 93d062c0e15b3d003797fa0d26bfaf81d54bb33f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 16:29:41 +1100 Subject: [PATCH 087/163] fixing tests --- stories/src/vertical/quote-app.jsx | 5 +++-- test/unit/state/hook-caller.spec.js | 30 +++++++++++++++++++++-------- test/utils/simple-state-preset.js | 7 ++++--- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index 136131f245..46f661b18b 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -64,10 +64,11 @@ export default class QuoteApp extends Component { onDragEnd = (result: DropResult, announce: Announce) => { if (result.reason === 'CANCEL') { announce('drop cancelled'); - } else { - announce('drop success'); + return; } + announce('drop success'); + publishOnDragEnd(result); // dropped outside the list diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index 9e8e589a72..89c8528b5c 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -1,16 +1,18 @@ // @flow import createHookCaller from '../../../src/state/hooks/hook-caller'; -import type { Hooks, HookCaller } from '../../../src/state/hooks/hooks-types'; +import type { HookCaller } from '../../../src/state/hooks/hooks-types'; import * as state from '../../utils/simple-state-preset'; import { getPreset } from '../../utils/dimension'; import noImpact, { noMovement } from '../../../src/state/no-impact'; import type { Announce, + Hooks, DropResult, State, DimensionState, DraggableLocation, DragStart, + DragUpdate, DragImpact, } from '../../../src/types'; @@ -116,11 +118,17 @@ describe('fire hooks', () => { }; describe('has not moved from home location', () => { - it('should not provide an update if the location has not changed since the last drag', () => { + beforeEach(() => { // start a drag - caller.onStateChange(hooks, state.requesting(), state.dragging()); + caller.onStateChange( + hooks, + state.requesting(), + withImpact(state.dragging(), inHomeImpact), + ); expect(hooks.onDragUpdate).not.toHaveBeenCalled(); + }); + it('should not provide an update if the location has not changed since the last drag', () => { // drag to the same spot caller.onStateChange( hooks, @@ -141,7 +149,7 @@ describe('fire hooks', () => { direction: preset.home.axis.direction, destination, }; - const expected: DropResult = { + const expected: DragUpdate = { draggableId: start.draggableId, type: start.type, source: start.source, @@ -170,7 +178,7 @@ describe('fire hooks', () => { direction: preset.home.axis.direction, destination, }; - const expected: DropResult = { + const expected: DragUpdate = { draggableId: start.draggableId, type: start.type, source: start.source, @@ -188,7 +196,7 @@ describe('fire hooks', () => { }); it('should provide an update if moving from a droppable to nothing', () => { - const expected: DropResult = { + const expected: DragUpdate = { draggableId: start.draggableId, type: start.type, source: start.source, @@ -521,6 +529,7 @@ describe('fire hooks', () => { droppableId: preset.inHome1.descriptor.droppableId, index: preset.inHome1.descriptor.index + 1, }, + reason: 'DROP', }; const current: State = { phase: 'DROP_COMPLETE', @@ -563,6 +572,7 @@ describe('fire hooks', () => { index: preset.inHome1.descriptor.index, }, destination: null, + reason: 'DROP', }; const current: State = { phase: 'DROP_COMPLETE', @@ -579,7 +589,7 @@ describe('fire hooks', () => { expect(hooks.onDragEnd).toHaveBeenCalledWith(result, announceMock); }); - it('should call onDragEnd with null if the item did not move', () => { + it('should call onDragEnd with original source if the item did not move', () => { const source: DraggableLocation = { droppableId: preset.inHome1.descriptor.droppableId, index: preset.inHome1.descriptor.index, @@ -589,6 +599,7 @@ describe('fire hooks', () => { type: preset.home.descriptor.type, source, destination: source, + reason: 'DROP', }; const current: State = { phase: 'DROP_COMPLETE', @@ -604,7 +615,8 @@ describe('fire hooks', () => { type: result.type, source: result.source, // destination has been cleared - destination: null, + destination: source, + reason: 'DROP', }; caller.onStateChange(hooks, previous, current); @@ -626,6 +638,7 @@ describe('fire hooks', () => { droppableId: preset.inHome1.descriptor.droppableId, }, destination: null, + reason: 'CANCEL', }; caller.onStateChange(hooks, state.dragging(), state.idle); @@ -659,6 +672,7 @@ describe('fire hooks', () => { droppableId: preset.inHome1.descriptor.droppableId, }, destination: null, + reason: 'CANCEL', }; caller.onStateChange(hooks, state.dropAnimating(), state.idle); diff --git a/test/utils/simple-state-preset.js b/test/utils/simple-state-preset.js index 4c76996b6f..5b490b2e59 100644 --- a/test/utils/simple-state-preset.js +++ b/test/utils/simple-state-preset.js @@ -14,7 +14,7 @@ import type { DragState, DropResult, PendingDrop, - DropTrigger, + DropReason, DraggableId, DragImpact, } from '../../src/types'; @@ -167,11 +167,10 @@ export const scrollJumpRequest = (request: Position): State => { return result; }; -const getDropAnimating = (id: DraggableId, trigger: DropTrigger): State => { +const getDropAnimating = (id: DraggableId, reason: DropReason): State => { const descriptor: DraggableDescriptor = preset.draggables[id].descriptor; const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor; const pending: PendingDrop = { - trigger, newHomeOffset: origin, impact: noImpact, result: { @@ -182,6 +181,7 @@ const getDropAnimating = (id: DraggableId, trigger: DropTrigger): State => { index: descriptor.index, }, destination: null, + reason, }, }; @@ -218,6 +218,7 @@ export const dropComplete = ( index: descriptor.index, }, destination: null, + reason: 'DROP', }; const value: State = { From c66c2b738efd7b89f0fca17542c2cddb99ab3cdd Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 19:27:27 +1100 Subject: [PATCH 088/163] adding some accessibility stories --- README.md | 25 ++++++++++++ src/state/hooks/hook-caller.js | 2 +- stories/8-accessibility-story.js | 9 +++++ stories/src/accessible/data.js | 19 +++++++++ stories/src/accessible/task-app.jsx | 59 ++++++++++++++++++++++++++++ stories/src/accessible/task-list.jsx | 38 ++++++++++++++++++ stories/src/accessible/task.jsx | 34 ++++++++++++++-- stories/src/vertical/quote-app.jsx | 16 ++++++-- test/unit/state/hook-caller.spec.js | 10 ++--- 9 files changed, 198 insertions(+), 14 deletions(-) create mode 100644 stories/8-accessibility-story.js create mode 100644 stories/src/accessible/data.js create mode 100644 stories/src/accessible/task-app.jsx diff --git a/README.md b/README.md index b339cad717..c7c15e47e8 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ Traditionally drag and drop interactions have been exclusively a mouse or touch In addition to supporting keyboard, we have also audited how the keyboard shortcuts interact with standard browser keyboard interactions. When the user is not dragging they can use their keyboard as they normally would. While dragging we override and disable certain browser shortcuts (such as `tab`) to ensure a fluid experience for the user. +We also provide **support for screen readers** through the `announce` function provided to all of the `hooks`. This means that users who have visual (or other) impairments are able to complete all drag and drop operations through keyboard and audio feedback. + ## Mouse dragging ### Sloppy clicks and click blocking 🐱🎁 @@ -564,6 +566,29 @@ Here are a few poor user experiences that can occur if you change things *during - If you remove the node that the user is dragging, then the drag will instantly end - If you change the dimension of the dragging node, then other things will not move out of the way at the correct time. +### Accessibility ❤️ + +All of our lifecycle `hooks` provide the ability to `announce` a change to screen readers. It is a function that accepts a `string` and will print it to the user: + +```js +export type Announce = (message: string) => void; +``` + +Based on the information passed to in the `hooks` you are able to provide meaningful messages to screen readers. + +On lift + +On update +- item has moved index +- item has moved droppable +- item is no longer over a droppable (only possible with pointer based dragging) + +onDragEnd +- item was dropped in a new location +- item was dropped in the same location it started in +- item was dropped while in no location (only possible with pointer based dragging) +- drag was cancelled (may be due a user directly cancelling, a user cancelling indirectly through an action such as a browser resize, or an error). + #### Force focus after a transition between lists When an item is moved from one list to a different list, it loses browser focus if it had it. This is because `React` creates a new node in this situation. It will not lose focus if transitioned within the same list. The dragging item will always have had browser focus if it is dragging with a keyboard. It is highly recommended that you give the item (which is now in a different list) focus again. You can see an example of how to do this in our stories. Here is an example of how you could do it: diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index 46aa47d3f8..b36037a177 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -213,7 +213,6 @@ export default (announce: Announce): HookCaller => { index: descriptor.index, droppableId: descriptor.droppableId, }; - const result: DropResult = { draggableId: descriptor.id, type: home.descriptor.type, @@ -221,6 +220,7 @@ export default (announce: Announce): HookCaller => { destination: null, reason: 'CANCEL', }; + onDragEnd(result, announce); return; } diff --git a/stories/8-accessibility-story.js b/stories/8-accessibility-story.js new file mode 100644 index 0000000000..cfda0cc888 --- /dev/null +++ b/stories/8-accessibility-story.js @@ -0,0 +1,9 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import TaskApp from './src/accessible/task-app'; + +storiesOf('Accessibility', module) + .add('single list', () => ( + + )); diff --git a/stories/src/accessible/data.js b/stories/src/accessible/data.js new file mode 100644 index 0000000000..e7f6056f0b --- /dev/null +++ b/stories/src/accessible/data.js @@ -0,0 +1,19 @@ +// @flow +import type { Task } from './types'; + +const tasks: Task[] = [ + { + id: '1', + content: 'Eat lunch', + }, + { + id: '2', + content: 'Finish that book I have been reading', + }, + { + id: '3', + content: 'Go to the store', + }, +]; + +export default tasks; diff --git a/stories/src/accessible/task-app.jsx b/stories/src/accessible/task-app.jsx new file mode 100644 index 0000000000..ccd59e3ee0 --- /dev/null +++ b/stories/src/accessible/task-app.jsx @@ -0,0 +1,59 @@ +// @flow +import React, { Component } from 'react'; +import TaskList from './task-list'; +import initial from './data'; +import reorder from '../reorder'; +import { DragDropContext } from '../../../src/'; +import type { + DragStart, + DragUpdate, + DragResult, + Announce, +} from '../../../src/'; +import type { Task } from './types'; + +type State = {| + tasks: Task[] +|} + +export default class TaskApp extends Component<*, State> { + state: State = { + tasks: initial, + } + + onDragStart = (start: DragStart, announce: Announce) => { + + } + + onDragUpdate = (update: DragUpdate, announce: Announce) => { + + } + + onDragEnd = (result: DragResult, announce: Announce) => { + if (!result.destination) { + return; + } + + const tasks: Task[] = reorder( + this.state.tasks, + result.source.index, + result.destination.index + ); + + this.setState({ + tasks, + }); + } + + render() { + return ( + + + + ); + } +} diff --git a/stories/src/accessible/task-list.jsx b/stories/src/accessible/task-list.jsx index ee165f4089..d9e47245d5 100644 --- a/stories/src/accessible/task-list.jsx +++ b/stories/src/accessible/task-list.jsx @@ -1,2 +1,40 @@ // @flow +import React, { Component } from 'react'; +import styled from 'styled-components'; +import { Droppable } from '../../../src/'; +import Task from './task'; +import type { DroppableProvided, DroppableStateSnapshot } from '../../../src/'; +import type { Task as TaskType } from './types'; +type Props = {| + tasks: TaskType[], +|} + +const Container = styled.div` + background: lightgreen; + width: 400px; +`; + +export default class TaskList extends Component { + render() { + return ( + + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( + + {this.props.tasks.map((task: TaskType, index: number) => ( + + ))} + {provided.placeholder} + + )} + + ); + } +} diff --git a/stories/src/accessible/task.jsx b/stories/src/accessible/task.jsx index 68bd1ff213..3ef2a4c84a 100644 --- a/stories/src/accessible/task.jsx +++ b/stories/src/accessible/task.jsx @@ -1,20 +1,46 @@ // @flow import React, { Component } from 'react'; import styled from 'styled-components'; -import type { Task } from './types'; +import { Draggable } from '../../../src/'; +import type { DraggableProvided, DraggableStateSnapshot } from '../../../src/'; +import type { Task as TaskType } from './types'; +import { grid, borderRadius } from '../constants'; type Props = {| - task: Task + task: TaskType, + index: number, |} const Container = styled.div` background: lightblue; + padding: ${grid}px; + margin-bottom: ${grid}px; + border-radius: ${borderRadius}px; + font-size: 24px; `; -export class Task extends Component { +const Wrapper = styled.div``; + +export default class Task extends Component { render() { + const task: TaskType = this.props.task; + const index: number = this.props.index; + return ( - {this.props.task.content} + + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( + + + {this.props.task.content} + + {provided.placeholder} + + )} + ); } } diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index 46f661b18b..15f44ef6e3 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -42,8 +42,8 @@ export default class QuoteApp extends Component { onDragStart = (initial: DragStart, announce: Announce) => { announce(` - You have started dragging the quote in position ${initial.source.index + 1} of ${this.state.quotes.length}. - You can use your arrow keys to move the quote around, space bar to drop, and escape to cancel. + Item lifted. ${initial.source.index + 1} of ${this.state.quotes.length} in the list. + Use the arrow keys to move, space bar to drop, and escape to cancel. `); publishOnDragStart(initial); // Add a little vibration if the browser supports it. @@ -58,7 +58,7 @@ export default class QuoteApp extends Component { announce('You are currently not dragging over any droppable area'); return; } - announce(`You have moved the quote into position ${update.destination.index + 1} of ${this.state.quotes.length}`); + announce(`Now ${update.destination.index + 1} of ${this.state.quotes.length} in the list`); } onDragEnd = (result: DropResult, announce: Announce) => { @@ -67,7 +67,15 @@ export default class QuoteApp extends Component { return; } - announce('drop success'); + if (!result.destination) { + announce(` + Item has been dropped while not over a location. + It has been returned to its original position of ${result.source.index + 1} of ${this.state.quotes.length} + `); + return; + } + + announce(`Item dropped. It has moved from position ${result.source.index + 1} to ${result.destination.index + 1}`); publishOnDragEnd(result); diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index 89c8528b5c..038922a464 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -269,7 +269,7 @@ describe('fire hooks', () => { direction: preset.home.axis.direction, destination, }; - const expected: DropResult = { + const expected: DragUpdate = { draggableId: start.draggableId, type: start.type, source: start.source, @@ -296,7 +296,7 @@ describe('fire hooks', () => { direction: preset.home.axis.direction, destination, }; - const expected: DropResult = { + const expected: DragUpdate = { draggableId: start.draggableId, type: start.type, source: start.source, @@ -319,7 +319,7 @@ describe('fire hooks', () => { direction: null, destination: null, }; - const expected: DropResult = { + const expected: DragUpdate = { draggableId: start.draggableId, type: start.type, source: start.source, @@ -349,7 +349,7 @@ describe('fire hooks', () => { withImpact(state.dragging(), inHomeImpact), withImpact(state.dragging(), impact), ); - const first: DropResult = { + const first: DragUpdate = { draggableId: start.draggableId, type: start.type, source: start.source, @@ -364,7 +364,7 @@ describe('fire hooks', () => { withImpact(state.dragging(), impact), withImpact(state.dragging(), inHomeImpact), ); - const second: DropResult = { + const second: DragUpdate = { draggableId: start.draggableId, type: start.type, source: start.source, From d31ef1b9901e7bf244ffc96e71452e4f60ac7fba Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 20:44:33 +1100 Subject: [PATCH 089/163] pretty example --- src/view/announcer/announcer-types.js | 2 +- src/view/announcer/announcer.js | 29 +++++++++++++------ stories/src/accessible/task-app.jsx | 41 +++++++++++++++++++++++++-- stories/src/accessible/task-list.jsx | 33 +++++++++++++++------ stories/src/accessible/task.jsx | 10 +++++-- 5 files changed, 90 insertions(+), 25 deletions(-) diff --git a/src/view/announcer/announcer-types.js b/src/view/announcer/announcer-types.js index aaebd9f61f..2235b1005a 100644 --- a/src/view/announcer/announcer-types.js +++ b/src/view/announcer/announcer-types.js @@ -3,7 +3,7 @@ import type { Announce } from '../../types'; export type Announcer = {| announce: Announce, - describedBy: string, + id: string, mount: () => void, unmount: () => void, |} diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js index 88f8c3656b..d3f4c07c5b 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/announcer/announcer.js @@ -8,8 +8,24 @@ type State = {| let count: number = 0; +// https://allyjs.io/tutorials/hiding-elements.html +// Element is v hidden but is readable by screen readers +const visuallyHidden: Object = { + position: 'absolute', + width: '1px', + height: '1px', + margin: '-1px', + border: '0', + padding: '0', + overflow: 'hidden', + clip: 'rect(0 0 0 0)', + // for if 'clip' is ever removed + 'clip-path': 'inset(100%)', +}; + export default (): Announcer => { - const id: string = `data-react-beautiful-dnd-announcement-${count++}`; + const id: string = `react-beautiful-dnd-announcement-${count++}`; + // const id: string = 'react-beautiful-dnd-announcement'; let state: State = { el: null, @@ -48,13 +64,8 @@ export default (): Announcer => { // must read the whole thing every time el.setAttribute('aria-atomic', 'true'); - // style - el.style.position = 'absolute'; - el.style.top = '0px'; - el.style.fontSize = '30px'; - el.style.backgroundColor = 'rgba(255,255,255,0.4)'; - - // aria + // hide the element + Object.assign(el.style, visuallyHidden); if (!document.body) { throw new Error('Cannot find the head to append a style to'); @@ -88,7 +99,7 @@ export default (): Announcer => { const announcer: Announcer = { announce, - describedBy: id, + id, mount, unmount, }; diff --git a/stories/src/accessible/task-app.jsx b/stories/src/accessible/task-app.jsx index ccd59e3ee0..9d2ece664a 100644 --- a/stories/src/accessible/task-app.jsx +++ b/stories/src/accessible/task-app.jsx @@ -16,24 +16,56 @@ type State = {| tasks: Task[] |} +const getPosition = (index: number, length: number): string => ` + ${index + 1} of ${length} in the list +`.trim(); + +const itemReturned = (index: number, length: number): string => ` + Item has returned to ${getPosition(index, length)} +`.trim(); + export default class TaskApp extends Component<*, State> { state: State = { tasks: initial, } onDragStart = (start: DragStart, announce: Announce) => { - + announce(` + Item lifted. ${getPosition(start.source.index, this.state.tasks.length)}. + Use the arrow keys to move, space bar to drop, and escape to cancel + `); } onDragUpdate = (update: DragUpdate, announce: Announce) => { - + if (!update.destination) { + announce('You are currently not dragging over any droppable area'); + return; + } + announce(`Now ${getPosition(update.destination.index, this.state.tasks.length)}`); } onDragEnd = (result: DragResult, announce: Announce) => { + if (result.reason === 'CANCEL') { + announce(` + Movement cancelled. + ${itemReturned(result.source.index, this.state.tasks.length)} + `); + return; + } + if (!result.destination) { + announce(` + Item has been dropped while not over a location. + ${itemReturned(result.source.index, this.state.tasks.length)} + `); return; } + announce(` + Item dropped. + It has moved from ${result.source.index + 1} to ${result.destination.index + 1} + `); + const tasks: Task[] = reorder( this.state.tasks, result.source.index, @@ -52,7 +84,10 @@ export default class TaskApp extends Component<*, State> { onDragUpdate={this.onDragUpdate} onDragEnd={this.onDragEnd} > - + ); } diff --git a/stories/src/accessible/task-list.jsx b/stories/src/accessible/task-list.jsx index d9e47245d5..b8f9e208cc 100644 --- a/stories/src/accessible/task-list.jsx +++ b/stories/src/accessible/task-list.jsx @@ -5,14 +5,26 @@ import { Droppable } from '../../../src/'; import Task from './task'; import type { DroppableProvided, DroppableStateSnapshot } from '../../../src/'; import type { Task as TaskType } from './types'; +import { colors, grid, borderRadius } from '../constants'; type Props = {| tasks: TaskType[], + title: string, |} const Container = styled.div` - background: lightgreen; - width: 400px; + width: 300px; + background-color: ${colors.grey}; + border-radius: ${borderRadius}px; +`; + +const Title = styled.h3` + font-weight: bold; + padding: ${grid}px; +`; + +const List = styled.div` + padding: ${grid}px; `; export default class TaskList extends Component { @@ -24,13 +36,16 @@ export default class TaskList extends Component { innerRef={provided.innerRef} {...provided.droppableProps} > - {this.props.tasks.map((task: TaskType, index: number) => ( - - ))} + {this.props.title} + + {this.props.tasks.map((task: TaskType, index: number) => ( + + ))} + {provided.placeholder} )} diff --git a/stories/src/accessible/task.jsx b/stories/src/accessible/task.jsx index 3ef2a4c84a..4bcd979319 100644 --- a/stories/src/accessible/task.jsx +++ b/stories/src/accessible/task.jsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import { Draggable } from '../../../src/'; import type { DraggableProvided, DraggableStateSnapshot } from '../../../src/'; import type { Task as TaskType } from './types'; -import { grid, borderRadius } from '../constants'; +import { colors, grid, borderRadius } from '../constants'; type Props = {| task: TaskType, @@ -12,11 +12,14 @@ type Props = {| |} const Container = styled.div` - background: lightblue; + border-bottom: 1px solid #ccc; + background: ${colors.white}; padding: ${grid}px; margin-bottom: ${grid}px; border-radius: ${borderRadius}px; - font-size: 24px; + font-size: 18px; + + ${({ isDragging }) => (isDragging ? 'box-shadow: 1px 1px 1px grey' : '')} `; const Wrapper = styled.div``; @@ -32,6 +35,7 @@ export default class Task extends Component { From bf92c130c82d2d6abebaf8465972b169e0c8112b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 8 Feb 2018 21:13:24 +1100 Subject: [PATCH 090/163] improvements --- src/view/drag-handle/drag-handle-types.js | 3 +++ src/view/drag-handle/drag-handle.jsx | 1 + src/view/droppable/droppable-types.js | 2 ++ src/view/droppable/droppable.jsx | 1 + stories/src/accessible/task-app.jsx | 17 +++++++++++++---- stories/src/accessible/task-list.jsx | 5 ++++- 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/drag-handle/drag-handle-types.js index 556a79cf51..0b56b19cff 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/drag-handle/drag-handle-types.js @@ -31,6 +31,9 @@ export type DragHandleProps = {| // Control styling from style marshal 'data-react-beautiful-dnd-drag-handle': string, + // Aria role (nicer screen reader text) + role: string, + // Allow tabbing to this element tabIndex: number, diff --git a/src/view/drag-handle/drag-handle.jsx b/src/view/drag-handle/drag-handle.jsx index 7eea5b3155..94628471b1 100644 --- a/src/view/drag-handle/drag-handle.jsx +++ b/src/view/drag-handle/drag-handle.jsx @@ -192,6 +192,7 @@ export default class DragHandle extends Component { onClick: this.onClick, tabIndex: 0, 'data-react-beautiful-dnd-drag-handle': this.styleContext, + role: 'option', draggable: false, onDragStart: getFalse, onDrop: getFalse, diff --git a/src/view/droppable/droppable-types.js b/src/view/droppable/droppable-types.js index 0e11225dca..72ac12ea09 100644 --- a/src/view/droppable/droppable-types.js +++ b/src/view/droppable/droppable-types.js @@ -10,6 +10,8 @@ import type { export type DroppableProps = {| // used for shared global styles 'data-react-beautiful-dnd-droppable': string, + // used for improved screen reader messaging + role: string, |} export type Provided = {| diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 8be049cd9f..c6698e4cd6 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -102,6 +102,7 @@ export default class Droppable extends Component { placeholder: this.getPlaceholder(), droppableProps: { 'data-react-beautiful-dnd-droppable': this.styleContext, + role: 'listbox', }, }; const snapshot: StateSnapshot = { diff --git a/stories/src/accessible/task-app.jsx b/stories/src/accessible/task-app.jsx index 9d2ece664a..b90459ffd7 100644 --- a/stories/src/accessible/task-app.jsx +++ b/stories/src/accessible/task-app.jsx @@ -1,5 +1,6 @@ // @flow import React, { Component } from 'react'; +import styled from 'styled-components'; import TaskList from './task-list'; import initial from './data'; import reorder from '../reorder'; @@ -16,6 +17,12 @@ type State = {| tasks: Task[] |} +const PositionNicely = styled.div` + display: flex; + justify-content: center; + margin-top: 20vh; +`; + const getPosition = (index: number, length: number): string => ` ${index + 1} of ${length} in the list `.trim(); @@ -84,10 +91,12 @@ export default class TaskApp extends Component<*, State> { onDragUpdate={this.onDragUpdate} onDragEnd={this.onDragEnd} > - + + + ); } diff --git a/stories/src/accessible/task-list.jsx b/stories/src/accessible/task-list.jsx index b8f9e208cc..01572c03f3 100644 --- a/stories/src/accessible/task-list.jsx +++ b/stories/src/accessible/task-list.jsx @@ -25,13 +25,16 @@ const Title = styled.h3` const List = styled.div` padding: ${grid}px; + padding-bottom: 0px; + display: flex; + flex-direction: column; `; export default class TaskList extends Component { render() { return ( - {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( + {(provided: DroppableProvided) => ( Date: Thu, 8 Feb 2018 21:47:15 +1100 Subject: [PATCH 091/163] improving accessibility story --- src/view/announcer/announcer.js | 3 +- stories/src/accessible/task-app.jsx | 62 +++++++++++++++++++++++---- stories/src/primatives/quote-item.jsx | 4 +- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js index d3f4c07c5b..4e37a468a9 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/announcer/announcer.js @@ -41,9 +41,8 @@ export default (): Announcer => { return; } - state.el.textContent = message; + state.el.textContent = message.trim(); console.log(`%c ${message}`, 'color: green; font-size: 20px;'); - console.log(state.el); }; const mount = () => { diff --git a/stories/src/accessible/task-app.jsx b/stories/src/accessible/task-app.jsx index b90459ffd7..8f95e371e3 100644 --- a/stories/src/accessible/task-app.jsx +++ b/stories/src/accessible/task-app.jsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import TaskList from './task-list'; import initial from './data'; import reorder from '../reorder'; +import { grid } from '../constants'; import { DragDropContext } from '../../../src/'; import type { DragStart, @@ -14,15 +15,40 @@ import type { import type { Task } from './types'; type State = {| - tasks: Task[] + tasks: Task[], + blur: number, |} -const PositionNicely = styled.div` +const Container = styled.div` + margin-top: 20vh; display: flex; - justify-content: center; + flex-direction: column; + align-items: center; +`; +const Blur = styled.div` + filter: blur(${props => props.amount}px); +`; + +const BlurControls = styled.div` + display: flex; + align-items: center; + font-size: 20px; margin-top: 20vh; `; +const BlurTitle = styled.h4` + margin: 0; + +`; + +const Button = styled.button` + height: ${grid * 5}px; + width: ${grid * 5}px; + font-size: 20px; + justify-content: center; + margin: 0 ${grid * 2}px +`; + const getPosition = (index: number, length: number): string => ` ${index + 1} of ${length} in the list `.trim(); @@ -34,6 +60,7 @@ const itemReturned = (index: number, length: number): string => ` export default class TaskApp extends Component<*, State> { state: State = { tasks: initial, + blur: 0, } onDragStart = (start: DragStart, announce: Announce) => { @@ -91,12 +118,29 @@ export default class TaskApp extends Component<*, State> { onDragUpdate={this.onDragUpdate} onDragEnd={this.onDragEnd} > - - - + + + + + + + Blur + + + ); } diff --git a/stories/src/primatives/quote-item.jsx b/stories/src/primatives/quote-item.jsx index c5d6d358e6..71a268f6c9 100644 --- a/stories/src/primatives/quote-item.jsx +++ b/stories/src/primatives/quote-item.jsx @@ -15,7 +15,7 @@ type Props = { type HTMLElement = any; -const Container = styled.div` +const Container = styled.a` border-radius: ${borderRadius}px; border: 1px solid grey; background-color: ${({ isDragging }) => (isDragging ? colors.green : colors.white)}; @@ -116,7 +116,7 @@ export default class QuoteItem extends React.PureComponent { return ( Date: Thu, 8 Feb 2018 22:07:25 +1100 Subject: [PATCH 092/163] adding tests for announcer --- src/view/announcer/announcer.js | 14 ++--- test/unit/view/annoucer.spec.js | 107 ++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 test/unit/view/annoucer.spec.js diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js index 4e37a468a9..6d413b7a09 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/announcer/announcer.js @@ -36,13 +36,13 @@ export default (): Announcer => { }; const announce: Announce = (message: string): void => { - if (!state.el) { + const el: ?HTMLElement = state.el; + if (!el) { console.error('Cannot announce to unmounted node'); return; } - state.el.textContent = message.trim(); - console.log(`%c ${message}`, 'color: green; font-size: 20px;'); + el.textContent = message; }; const mount = () => { @@ -63,7 +63,7 @@ export default (): Announcer => { // must read the whole thing every time el.setAttribute('aria-atomic', 'true'); - // hide the element + // hide the element visually Object.assign(el.style, visuallyHidden); if (!document.body) { @@ -82,18 +82,18 @@ export default (): Announcer => { console.error('Will not unmount annoucer as it is already unmounted'); return; } - const previous = state.el; + const node: HTMLElement = state.el; setState({ el: null, }); - if (!previous.parentNode) { + if (!node.parentNode) { console.error('Cannot unmount style marshal as cannot find parent'); return; } - previous.parentNode.removeChild(previous); + node.parentNode.removeChild(node); }; const announcer: Announcer = { diff --git a/test/unit/view/annoucer.spec.js b/test/unit/view/annoucer.spec.js new file mode 100644 index 0000000000..114d9685c2 --- /dev/null +++ b/test/unit/view/annoucer.spec.js @@ -0,0 +1,107 @@ +// @flow +import createAnnouncer from '../../../src/view/announcer/announcer'; +import type { Announcer } from '../../../src/view/announcer/announcer-types'; + +describe('announcer', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { }); + }); + + afterEach(() => { + console.error.mockRestore(); + }); + + describe('mounting', () => { + it('should not create a dom node before mount is called', () => { + const announcer: Announcer = createAnnouncer(); + + const el: ?HTMLElement = document.getElementById(announcer.id); + + expect(el).not.toBeTruthy(); + }); + + it('should create a new element when mounting', () => { + const announcer: Announcer = createAnnouncer(); + + announcer.mount(); + const el: ?HTMLElement = document.getElementById(announcer.id); + + expect(el).toBeInstanceOf(HTMLElement); + }); + + it('should error if attempting to double mount', () => { + const announcer: Announcer = createAnnouncer(); + + announcer.mount(); + expect(console.error).not.toHaveBeenCalled(); + + announcer.mount(); + expect(console.error).toHaveBeenCalled(); + }); + + it('should apply the appropriate aria attributes and non visibility styles', () => { + const announcer: Announcer = createAnnouncer(); + + announcer.mount(); + const el: HTMLElement = (document.getElementById(announcer.id) : any); + + expect(el.getAttribute('aria-live')).toBe('assertive'); + expect(el.getAttribute('role')).toBe('log'); + expect(el.getAttribute('aria-atomic')).toBe('true'); + + // not checking all the styles - just enough to know we are doing something + expect(el.style.overflow).toBe('hidden'); + }); + }); + + describe('unmounting', () => { + it('should remove the element when unmounting', () => { + const announcer: Announcer = createAnnouncer(); + + announcer.mount(); + announcer.unmount(); + const el: ?HTMLElement = document.getElementById(announcer.id); + + expect(el).not.toBeTruthy(); + }); + + it('should error if attempting to unmount before mounting', () => { + const announcer: Announcer = createAnnouncer(); + + announcer.unmount(); + + expect(console.error).toHaveBeenCalled(); + }); + + it('should error if unmounting after an unmount', () => { + const announcer: Announcer = createAnnouncer(); + + announcer.mount(); + announcer.unmount(); + expect(console.error).not.toHaveBeenCalled(); + + announcer.unmount(); + expect(console.error).toHaveBeenCalled(); + }); + }); + + describe('announcing', () => { + it('should error if not mounted', () => { + const announcer: Announcer = createAnnouncer(); + + announcer.announce('test'); + + expect(console.error).toHaveBeenCalled(); + }); + + it('should set the text content of the announcement element', () => { + const announcer: Announcer = createAnnouncer(); + announcer.mount(); + const el: HTMLElement = (document.getElementById(announcer.id) : any); + + announcer.announce('test'); + + expect(el.textContent).toBe('test'); + }); + }); +}); From 4bf2486c428e434896d8c3bd58f461c3c0ed1a64 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 9 Feb 2018 07:41:47 +1100 Subject: [PATCH 093/163] progress --- src/index.js | 1 + stories/src/accessible/task-app.jsx | 13 ++++++++----- test/unit/integration/hooks-integration.spec.js | 2 ++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 5f1c1fee3f..23ef71c2a8 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,7 @@ export type { // Hooks DragStart, + DragUpdate, DropResult, DraggableLocation, Announce, diff --git a/stories/src/accessible/task-app.jsx b/stories/src/accessible/task-app.jsx index 8f95e371e3..429ed244e1 100644 --- a/stories/src/accessible/task-app.jsx +++ b/stories/src/accessible/task-app.jsx @@ -9,8 +9,9 @@ import { DragDropContext } from '../../../src/'; import type { DragStart, DragUpdate, - DragResult, + DropResult, Announce, + DraggableLocation, } from '../../../src/'; import type { Task } from './types'; @@ -78,7 +79,7 @@ export default class TaskApp extends Component<*, State> { announce(`Now ${getPosition(update.destination.index, this.state.tasks.length)}`); } - onDragEnd = (result: DragResult, announce: Announce) => { + onDragEnd = (result: DropResult, announce: Announce) => { if (result.reason === 'CANCEL') { announce(` Movement cancelled. @@ -87,7 +88,9 @@ export default class TaskApp extends Component<*, State> { return; } - if (!result.destination) { + const desination: ?DraggableLocation = result.destination; + + if (!desination) { announce(` Item has been dropped while not over a location. ${itemReturned(result.source.index, this.state.tasks.length)} @@ -97,13 +100,13 @@ export default class TaskApp extends Component<*, State> { announce(` Item dropped. - It has moved from ${result.source.index + 1} to ${result.destination.index + 1} + It has moved from ${result.source.index + 1} to ${desination.index + 1} `); const tasks: Task[] = reorder( this.state.tasks, result.source.index, - result.destination.index + desination.index, ); this.setState({ diff --git a/test/unit/integration/hooks-integration.spec.js b/test/unit/integration/hooks-integration.spec.js index 42a10bd311..eaea69e84f 100644 --- a/test/unit/integration/hooks-integration.spec.js +++ b/test/unit/integration/hooks-integration.spec.js @@ -196,6 +196,7 @@ describe('hooks integration', () => { type: 'DEFAULT', source, destination: null, + reason: 'DROP', }; const cancelled: DropResult = { @@ -203,6 +204,7 @@ describe('hooks integration', () => { type: 'DEFAULT', source, destination: null, + reason: 'CANCEL', }; return { start, completed, cancelled }; From 1af4d22429fd625f16ba1ac3fb0687a6a20d87d9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 9 Feb 2018 09:08:49 +1100 Subject: [PATCH 094/163] adding default screen reader messages --- src/index.js | 1 - src/state/action-creators.js | 19 ++++++ .../dimension-marshal-types.js | 4 ++ .../dimension-marshal/dimension-marshal.js | 12 ++-- src/state/hooks/hook-caller.js | 38 +++++++++--- src/state/hooks/message-preset.js | 60 +++++++++++++++++++ src/state/reducer.js | 37 ++++++++++++ src/types.js | 8 ++- src/view/announcer/announcer.js | 1 + .../drag-drop-context/drag-drop-context.jsx | 4 ++ stories/src/accessible/task-app.jsx | 40 ++++++------- 11 files changed, 183 insertions(+), 41 deletions(-) create mode 100644 src/state/hooks/message-preset.js diff --git a/src/index.js b/src/index.js index 23ef71c2a8..05abcc79b8 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,6 @@ export type { DragUpdate, DropResult, DraggableLocation, - Announce, } from './types'; // Droppable diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 11a86337dd..694daf4f19 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -102,6 +102,24 @@ export const publishDroppableDimensions = payload: dimensions, }); +export type BulkPublishDimensionsAction = {| + type: 'BULK_DIMENSION_PUBLISH', + payload: {| + draggables: DraggableDimension[], + droppables: DroppableDimension[], + |} +|} + +export const bulkPublishDimensions = ( + draggables: DraggableDimension[], + droppables: DroppableDimension[] +): BulkPublishDimensionsAction => ({ + type: 'BULK_DIMENSION_PUBLISH', + payload: { + draggables, droppables, + }, +}); + export type UpdateDroppableDimensionScrollAction = {| type: 'UPDATE_DROPPABLE_DIMENSION_SCROLL', payload: { @@ -492,6 +510,7 @@ export type Action = RequestDimensionsAction | PublishDraggableDimensionsAction | PublishDroppableDimensionsAction | + BulkPublishDimensionsAction | UpdateDroppableDimensionScrollAction | UpdateDroppableDimensionIsEnabledAction | MoveByWindowScrollAction | diff --git a/src/state/dimension-marshal/dimension-marshal-types.js b/src/state/dimension-marshal/dimension-marshal-types.js index 5f17049a5d..a6e3cf0af6 100644 --- a/src/state/dimension-marshal/dimension-marshal-types.js +++ b/src/state/dimension-marshal/dimension-marshal-types.js @@ -68,6 +68,10 @@ export type Callbacks = {| cancel: () => void, publishDraggables: (DraggableDimension[]) => void, publishDroppables: (DroppableDimension[]) => void, + bulkPublish: ( + draggables: DraggableDimension[], + droppables: DroppableDimension[], + ) => void, updateDroppableScroll: (id: DroppableId, newScroll: Position) => void, updateDroppableIsEnabled: (id: DroppableId, isEnabled: boolean) => void, |} diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index c6e6f6378c..240c59fa08 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -368,12 +368,10 @@ export default (callbacks: Callbacks) => { }, { draggables: [], droppables: [] } ); - if (toBePublished.droppables.length) { - callbacks.publishDroppables(toBePublished.droppables); - } - if (toBePublished.draggables.length) { - callbacks.publishDraggables(toBePublished.draggables); - } + callbacks.bulkPublish( + toBePublished.draggables, + toBePublished.droppables, + ); // need to watch the scroll on each droppable toBePublished.droppables.forEach((dimension: DroppableDimension) => { @@ -381,6 +379,8 @@ export default (callbacks: Callbacks) => { entry.callbacks.watchScroll(); }); + // callbacks.initialCollectionComplete(); + setFrameId(null); }); diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index b36037a177..cf3f87b1f0 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -1,4 +1,5 @@ // @flow +import messagePreset from './message-preset'; import type { HookCaller } from './hooks-types'; import type { Announce, @@ -128,9 +129,15 @@ export default (announce: Announce): HookCaller => { hasMovedFromStartLocation: true, }); - if (onDragUpdate) { - onDragUpdate(update, announce); + console.log('has moved from start!'); + if (!onDragUpdate) { + announce(messagePreset.onDragUpdate(update)); + return; } + + const message: ?string = onDragUpdate(update); + announce(message || messagePreset.onDragUpdate(update)); + return; } @@ -143,9 +150,14 @@ export default (announce: Announce): HookCaller => { lastDestination: destination, }); - if (onDragUpdate) { - onDragUpdate(update, announce); + if (!onDragUpdate) { + announce(messagePreset.onDragUpdate(update)); + return; } + + const message: ?string = onDragUpdate(update); + announce(message || messagePreset.onDragUpdate(update)); + return; } @@ -177,10 +189,13 @@ export default (announce: Announce): HookCaller => { // onDragStart is optional if (!onDragStart) { + announce(messagePreset.onDragStart(start)); return; } - onDragStart(start, announce); + const message: ?string = onDragStart(start); + + announce(message || messagePreset.onDragStart(start)); return; } @@ -190,8 +205,11 @@ export default (announce: Announce): HookCaller => { console.error('cannot fire onDragEnd hook without drag state', { current, previous }); return; } + const result: DropResult = current.drop.result; + const message: ?string = onDragEnd(result); - onDragEnd(current.drop.result, announce); + announce(message || messagePreset.onDragEnd(result)); + return; } // Drag ended while dragging @@ -221,7 +239,9 @@ export default (announce: Announce): HookCaller => { reason: 'CANCEL', }; - onDragEnd(result, announce); + const message: ?string = onDragEnd(result); + + announce(message || messagePreset.onDragEnd(result)); return; } @@ -240,7 +260,9 @@ export default (announce: Announce): HookCaller => { destination: null, reason: 'CANCEL', }; - onDragEnd(result, announce); + const message: ?string = onDragEnd(result); + + announce(message || messagePreset.onDragEnd(result)); } }; diff --git a/src/state/hooks/message-preset.js b/src/state/hooks/message-preset.js new file mode 100644 index 0000000000..487b3ab31f --- /dev/null +++ b/src/state/hooks/message-preset.js @@ -0,0 +1,60 @@ +// @flow +import type { + DragStart, + DragUpdate, + DropResult, +} from '../../types'; + +export type MessagePreset = {| + onDragStart: (start: DragStart) => string, + onDragUpdate: (update: DragUpdate) => string, + onDragEnd: (result: DropResult) => string, +|} + +const onDragStart = (start: DragStart): string => ` + You have lifted an item in position ${start.source.index + 1}. + Use the arrow keys to move, space bar to drop, and escape to cancel. +`; + +const onDragUpdate = (update: DragUpdate): string => { + if (!update.destination) { + return 'You are currently not dragging over any droppable area'; + } + // TODO: list what droppable they are in? + return `Item is now in position ${update.destination.index + 1}`; +}; + +const onDragEnd = (result: DropResult): string => { + if (result.reason === 'CANCEL') { + return ` + Movement cancelled. + Item has been returned to its original position of ${result.source.index + 1} + `; + } + + if (!result.destination) { + return ` + Item has been dropped while not over a location. + Item has been returned to its original position of ${result.source.index + 1} + `; + } + + if (result.source.droppableId === result.destination.droppableId) { + return ` + Item dropped. + It has moved from position ${result.source.index + 1} to ${result.destination.index + 1} + `; + } + + return ` + Item dropped. + It has moved from position ${result.source.index + 1} in list ${result.source.droppableId} + to position ${result.destination.index + 1} in list ${result.destination.droppableId} + `; +}; + +const preset: MessagePreset = { + onDragStart, onDragUpdate, onDragEnd, +}; + +export default preset; diff --git a/src/state/reducer.js b/src/state/reducer.js index 2e319cf0d5..e9c7722442 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -249,6 +249,43 @@ export default (state: State = clean('IDLE'), action: Action): State => { return updateStateAfterDimensionChange(newState); } + if (action.type === 'BULK_DIMENSION_PUBLISH') { + const draggables: DraggableDimension[] = action.payload.draggables; + const droppables: DroppableDimension[] = action.payload.droppables; + + if (!canPublishDimension(state.phase)) { + console.warn('dimensions rejected as no longer allowing dimension capture in phase', state.phase); + return state; + } + + const newDraggables: DraggableDimensionMap = draggables.reduce((previous, current) => { + previous[current.descriptor.id] = current; + return previous; + }, {}); + + const newDroppables: DroppableDimensionMap = droppables.reduce((previous, current) => { + previous[current.descriptor.id] = current; + return previous; + }, {}); + + const newState: State = { + ...state, + dimension: { + request: state.dimension.request, + draggable: { + ...state.dimension.draggable, + ...newDraggables, + }, + droppable: { + ...state.dimension.droppable, + ...newDroppables, + }, + }, + }; + + return updateStateAfterDimensionChange(newState); + } + if (action.type === 'COMPLETE_LIFT') { if (state.phase !== 'COLLECTING_INITIAL_DIMENSIONS') { console.error('trying complete lift without collecting dimensions'); diff --git a/src/types.js b/src/types.js index aadb15b8fa..12ceb31258 100644 --- a/src/types.js +++ b/src/types.js @@ -222,6 +222,8 @@ export type CurrentDrag = {| windowScroll: Position, // whether or not draggable movements should be animated shouldAnimate: boolean, + // has the initial dimension capture completed? + // isInitialDimensionCaptureCompleted: boolean, |} // published when a drag starts @@ -317,8 +319,8 @@ export type Store = ReduxStore; export type Announce = (message: string) => void; export type Hooks = {| - onDragStart?: (start: DragStart, announce: Announce) => void, - onDragUpdate?: (update: DragUpdate, announce: Announce) => void, - onDragEnd: (result: DropResult, announce: Announce) => void, + onDragStart?: (start: DragStart) => ?string, + onDragUpdate?: (update: DragUpdate) => ?string, + onDragEnd: (result: DropResult) => ?string, |} diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js index 6d413b7a09..8792170087 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/announcer/announcer.js @@ -43,6 +43,7 @@ export default (): Announcer => { } el.textContent = message; + console.log('announcing:', message); }; const mount = () => { diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index 714b641823..b89dfe837c 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -42,6 +42,7 @@ import { publishDroppableDimensions, updateDroppableDimensionScroll, updateDroppableDimensionIsEnabled, + bulkPublishDimensions, } from '../../state/action-creators'; type Props = {| @@ -117,6 +118,9 @@ export default class DragDropContext extends React.Component { publishDroppables: (dimensions: DroppableDimension[]) => { this.store.dispatch(publishDroppableDimensions(dimensions)); }, + bulkPublish: (draggables: DraggableDimension[], droppables: DroppableDimension[]) => { + this.store.dispatch(bulkPublishDimensions(draggables, droppables)); + }, updateDroppableScroll: (id: DroppableId, newScroll: Position) => { this.store.dispatch(updateDroppableDimensionScroll(id, newScroll)); }, diff --git a/stories/src/accessible/task-app.jsx b/stories/src/accessible/task-app.jsx index 429ed244e1..8af4a5d7dc 100644 --- a/stories/src/accessible/task-app.jsx +++ b/stories/src/accessible/task-app.jsx @@ -10,7 +10,6 @@ import type { DragStart, DragUpdate, DropResult, - Announce, DraggableLocation, } from '../../../src/'; import type { Task } from './types'; @@ -64,45 +63,35 @@ export default class TaskApp extends Component<*, State> { blur: 0, } - onDragStart = (start: DragStart, announce: Announce) => { - announce(` - Item lifted. ${getPosition(start.source.index, this.state.tasks.length)}. - Use the arrow keys to move, space bar to drop, and escape to cancel - `); - } + onDragStart = (start: DragStart): string => ` + Item lifted. ${getPosition(start.source.index, this.state.tasks.length)}. + Use the arrow keys to move, space bar to drop, and escape to cancel + ` - onDragUpdate = (update: DragUpdate, announce: Announce) => { + onDragUpdate = (update: DragUpdate): string => { if (!update.destination) { - announce('You are currently not dragging over any droppable area'); - return; + return 'You are currently not dragging over any droppable area'; } - announce(`Now ${getPosition(update.destination.index, this.state.tasks.length)}`); + return `Now ${getPosition(update.destination.index, this.state.tasks.length)}`; } - onDragEnd = (result: DropResult, announce: Announce) => { + onDragEnd = (result: DropResult): string => { if (result.reason === 'CANCEL') { - announce(` + return ` Movement cancelled. ${itemReturned(result.source.index, this.state.tasks.length)} - `); - return; + `; } const desination: ?DraggableLocation = result.destination; if (!desination) { - announce(` + return ` Item has been dropped while not over a location. ${itemReturned(result.source.index, this.state.tasks.length)} - `); - return; + `; } - announce(` - Item dropped. - It has moved from ${result.source.index + 1} to ${desination.index + 1} - `); - const tasks: Task[] = reorder( this.state.tasks, result.source.index, @@ -112,6 +101,11 @@ export default class TaskApp extends Component<*, State> { this.setState({ tasks, }); + + return ` + Item dropped. + It has moved from ${result.source.index + 1} to ${desination.index + 1} + `; } render() { From 6f01b7892650bdba07f7446b8f3b833a47862c31 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 9 Feb 2018 10:47:34 +1100 Subject: [PATCH 095/163] new api and cleanup --- src/index.js | 2 + src/state/hooks/hook-caller.js | 62 ++++++++++++++++----- src/state/hooks/message-preset.js | 10 ++-- src/types.js | 10 +++- src/view/drag-handle/drag-handle-types.js | 2 +- src/view/drag-handle/drag-handle.jsx | 3 +- src/view/droppable/droppable-types.js | 2 - src/view/droppable/droppable.jsx | 1 - stories/src/accessible/task-app.jsx | 68 ++++++++++++----------- 9 files changed, 101 insertions(+), 59 deletions(-) diff --git a/src/index.js b/src/index.js index 05abcc79b8..5eb671d441 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,8 @@ export type { DragStart, DragUpdate, DropResult, + HookProvided, + Announce, DraggableLocation, } from './types'; diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index cf3f87b1f0..5d52a477f5 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -4,6 +4,7 @@ import type { HookCaller } from './hooks-types'; import type { Announce, Hooks, + HookProvided, State as AppState, DragState, DragStart, @@ -44,6 +45,17 @@ const areLocationsEqual = (current: ?DraggableLocation, next: ?DraggableLocation current.index === next.index; }; +const getProvided = (announce: Announce): HookProvided => { + const detector = (message: string) => { + announce(message); + detector.wasCalled = true; + }; + + return { + announce: detector, + }; +}; + export default (announce: Announce): HookCaller => { let state: State = initial; @@ -129,14 +141,18 @@ export default (announce: Announce): HookCaller => { hasMovedFromStartLocation: true, }); - console.log('has moved from start!'); if (!onDragUpdate) { announce(messagePreset.onDragUpdate(update)); return; } - const message: ?string = onDragUpdate(update); - announce(message || messagePreset.onDragUpdate(update)); + const provided: HookProvided = getProvided(announce); + onDragUpdate(update, provided); + + // if they do not announce - use the default + if (!provided.announce.wasCalled) { + announce(messagePreset.onDragUpdate(update)); + } return; } @@ -155,8 +171,13 @@ export default (announce: Announce): HookCaller => { return; } - const message: ?string = onDragUpdate(update); - announce(message || messagePreset.onDragUpdate(update)); + const provided: HookProvided = getProvided(announce); + onDragUpdate(update, provided); + + // if they did not announce anything - use the default + if (!provided.announce.wasCalled) { + announce(messagePreset.onDragUpdate(update)); + } return; } @@ -193,9 +214,13 @@ export default (announce: Announce): HookCaller => { return; } - const message: ?string = onDragStart(start); + const provided: HookProvided = getProvided(announce); + onDragStart(start, provided); - announce(message || messagePreset.onDragStart(start)); + // if they did not announce anything - use the default + if (!provided.announce.wasCalled) { + announce(messagePreset.onDragStart(start)); + } return; } @@ -206,9 +231,13 @@ export default (announce: Announce): HookCaller => { return; } const result: DropResult = current.drop.result; - const message: ?string = onDragEnd(result); - announce(message || messagePreset.onDragEnd(result)); + const provided: HookProvided = getProvided(announce); + onDragEnd(result, provided); + + if (!provided.announce.wasCalled) { + announce(messagePreset.onDragEnd(result)); + } return; } @@ -239,9 +268,13 @@ export default (announce: Announce): HookCaller => { reason: 'CANCEL', }; - const message: ?string = onDragEnd(result); + const provided: HookProvided = getProvided(announce); + onDragEnd(result, provided); + + if (!provided.announce.wasCalled) { + announce(messagePreset.onDragEnd(result)); + } - announce(message || messagePreset.onDragEnd(result)); return; } @@ -260,9 +293,12 @@ export default (announce: Announce): HookCaller => { destination: null, reason: 'CANCEL', }; - const message: ?string = onDragEnd(result); + const provided: HookProvided = getProvided(announce); + onDragEnd(result, provided); - announce(message || messagePreset.onDragEnd(result)); + if (!provided.announce.wasCalled) { + announce(messagePreset.onDragEnd(result)); + } } }; diff --git a/src/state/hooks/message-preset.js b/src/state/hooks/message-preset.js index 487b3ab31f..6f8108b1c7 100644 --- a/src/state/hooks/message-preset.js +++ b/src/state/hooks/message-preset.js @@ -21,27 +21,27 @@ const onDragUpdate = (update: DragUpdate): string => { return 'You are currently not dragging over any droppable area'; } // TODO: list what droppable they are in? - return `Item is now in position ${update.destination.index + 1}`; + return `You have moved the item to position ${update.destination.index + 1}`; }; const onDragEnd = (result: DropResult): string => { if (result.reason === 'CANCEL') { return ` Movement cancelled. - Item has been returned to its original position of ${result.source.index + 1} + The item has returned to its starting position of ${result.source.index + 1} `; } if (!result.destination) { return ` - Item has been dropped while not over a location. - Item has been returned to its original position of ${result.source.index + 1} + The item has been dropped while not over a location. + The item has returned to its starting position of ${result.source.index + 1} `; } if (result.source.droppableId === result.destination.droppableId) { return ` - Item dropped. + You have dropped the item. It has moved from position ${result.source.index + 1} to ${result.destination.index + 1} `; } diff --git a/src/types.js b/src/types.js index 12ceb31258..28b2c5dd4d 100644 --- a/src/types.js +++ b/src/types.js @@ -318,9 +318,13 @@ export type Store = ReduxStore; export type Announce = (message: string) => void; +export type HookProvided = {| + announce: Announce, +|} + export type Hooks = {| - onDragStart?: (start: DragStart) => ?string, - onDragUpdate?: (update: DragUpdate) => ?string, - onDragEnd: (result: DropResult) => ?string, + onDragStart?: (start: DragStart, provided: HookProvided) => void, + onDragUpdate?: (update: DragUpdate, provided: HookProvided) => void, + onDragEnd: (result: DropResult, provided: HookProvided) => void, |} diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/drag-handle/drag-handle-types.js index 0b56b19cff..d310caab4b 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/drag-handle/drag-handle-types.js @@ -32,7 +32,7 @@ export type DragHandleProps = {| 'data-react-beautiful-dnd-drag-handle': string, // Aria role (nicer screen reader text) - role: string, + 'aria-roledescription': string, // Allow tabbing to this element tabIndex: number, diff --git a/src/view/drag-handle/drag-handle.jsx b/src/view/drag-handle/drag-handle.jsx index 94628471b1..8e6ddb6542 100644 --- a/src/view/drag-handle/drag-handle.jsx +++ b/src/view/drag-handle/drag-handle.jsx @@ -192,7 +192,8 @@ export default class DragHandle extends Component { onClick: this.onClick, tabIndex: 0, 'data-react-beautiful-dnd-drag-handle': this.styleContext, - role: 'option', + // English default. Consumers are welcome to add their own start instruction + 'aria-roledescription': 'Draggable item. Press space bar to lift', draggable: false, onDragStart: getFalse, onDrop: getFalse, diff --git a/src/view/droppable/droppable-types.js b/src/view/droppable/droppable-types.js index 72ac12ea09..0e11225dca 100644 --- a/src/view/droppable/droppable-types.js +++ b/src/view/droppable/droppable-types.js @@ -10,8 +10,6 @@ import type { export type DroppableProps = {| // used for shared global styles 'data-react-beautiful-dnd-droppable': string, - // used for improved screen reader messaging - role: string, |} export type Provided = {| diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index c6698e4cd6..8be049cd9f 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -102,7 +102,6 @@ export default class Droppable extends Component { placeholder: this.getPlaceholder(), droppableProps: { 'data-react-beautiful-dnd-droppable': this.styleContext, - role: 'listbox', }, }; const snapshot: StateSnapshot = { diff --git a/stories/src/accessible/task-app.jsx b/stories/src/accessible/task-app.jsx index 8af4a5d7dc..abcef4a02f 100644 --- a/stories/src/accessible/task-app.jsx +++ b/stories/src/accessible/task-app.jsx @@ -7,10 +7,12 @@ import reorder from '../reorder'; import { grid } from '../constants'; import { DragDropContext } from '../../../src/'; import type { + Announce, DragStart, DragUpdate, DropResult, DraggableLocation, + HookProvided, } from '../../../src/'; import type { Task } from './types'; @@ -20,7 +22,7 @@ type State = {| |} const Container = styled.div` - margin-top: 20vh; + padding-top: 20vh; display: flex; flex-direction: column; align-items: center; @@ -38,7 +40,6 @@ const BlurControls = styled.div` const BlurTitle = styled.h4` margin: 0; - `; const Button = styled.button` @@ -46,66 +47,67 @@ const Button = styled.button` width: ${grid * 5}px; font-size: 20px; justify-content: center; - margin: 0 ${grid * 2}px + margin: 0 ${grid * 2}px; + cursor: pointer; `; -const getPosition = (index: number, length: number): string => ` - ${index + 1} of ${length} in the list -`.trim(); - -const itemReturned = (index: number, length: number): string => ` - Item has returned to ${getPosition(index, length)} -`.trim(); - export default class TaskApp extends Component<*, State> { state: State = { tasks: initial, blur: 0, } - onDragStart = (start: DragStart): string => ` - Item lifted. ${getPosition(start.source.index, this.state.tasks.length)}. - Use the arrow keys to move, space bar to drop, and escape to cancel - ` + // in? + onDragStart = (start: DragStart, provided: HookProvided): void => provided.announce(` + You have lifted a task. + It is in position ${start.source.index + 1} of ${this.state.tasks.length} in the list. + Use the arrow keys to move, space bar to drop, and escape to cancel. + `) - onDragUpdate = (update: DragUpdate): string => { + onDragUpdate = (update: DragUpdate, provided: HookProvided): void => { + const announce: Announce = provided.announce; if (!update.destination) { - return 'You are currently not dragging over any droppable area'; + announce('You are currently not dragging over any droppable area'); + return; } - return `Now ${getPosition(update.destination.index, this.state.tasks.length)}`; + announce(`You have moved the task to position ${update.destination.index + 1}`); } - onDragEnd = (result: DropResult): string => { + onDragEnd = (result: DropResult, provided: HookProvided): void => { + const announce: Announce = provided.announce; + // TODO: not being called on cancel!!! if (result.reason === 'CANCEL') { - return ` + announce(` Movement cancelled. - ${itemReturned(result.source.index, this.state.tasks.length)} - `; + The task has returned to its starting position of ${result.source.index + 1} + `); + return; } - const desination: ?DraggableLocation = result.destination; + const destination: ?DraggableLocation = result.destination; - if (!desination) { - return ` - Item has been dropped while not over a location. - ${itemReturned(result.source.index, this.state.tasks.length)} - `; + if (!destination) { + announce(` + The task has been dropped while not over a location. + The task has returned to its starting position of ${result.source.index + 1} + `); + return; } const tasks: Task[] = reorder( this.state.tasks, result.source.index, - desination.index, + destination.index, ); this.setState({ tasks, }); - return ` - Item dropped. - It has moved from ${result.source.index + 1} to ${desination.index + 1} - `; + announce(` + You have dropped the task. + It has moved from position ${result.source.index + 1} to ${destination.index + 1} + `); } render() { From 761c1844e40dd59dc15e3e2461651328adaa85d8 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 07:47:24 +1100 Subject: [PATCH 096/163] adding some blue --- stories/src/accessible/task.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stories/src/accessible/task.jsx b/stories/src/accessible/task.jsx index 4bcd979319..e4687df5cc 100644 --- a/stories/src/accessible/task.jsx +++ b/stories/src/accessible/task.jsx @@ -19,7 +19,7 @@ const Container = styled.div` border-radius: ${borderRadius}px; font-size: 18px; - ${({ isDragging }) => (isDragging ? 'box-shadow: 1px 1px 1px grey' : '')} + ${({ isDragging }) => (isDragging ? 'box-shadow: 1px 1px 1px grey; background: lightblue' : '')} `; const Wrapper = styled.div``; From 9f3f162f2d9bd4ddb2e3ace838774cd5b0612948 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 09:17:34 +1100 Subject: [PATCH 097/163] jump scroll just got some love --- src/state/move-to-next-index/in-home-list.js | 52 ++++++++++++-------- src/state/reducer.js | 47 ++++++++++++------ src/view/announcer/announcer.js | 2 +- stories/1-single-vertical-list-story.js | 4 +- stories/src/vertical/quote-app.jsx | 34 ++----------- 5 files changed, 69 insertions(+), 70 deletions(-) diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 307fa59914..8d5ec4d708 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,10 +1,10 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch } from '../position'; +import { patch, subtract } from '../position'; import withDroppableScroll from '../with-droppable-scroll'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import getViewport from '../../window/get-viewport'; -import getScrollJumpResult from './get-scroll-jump-result'; +// import getScrollJumpResult from './get-scroll-jump-result'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; @@ -19,6 +19,8 @@ import type { Area, } from '../../types'; +const origin: Position = { x: 0, y: 0 }; + export default ({ isMovingForward, draggableId, @@ -83,22 +85,6 @@ export default ({ destinationAxis: droppable.axis, }); - const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newPageCenter, - viewport, - }); - - if (!isVisibleInNewLocation) { - return getScrollJumpResult({ - newPageCenter, - previousPageCenter, - droppable, - previousImpact, - }); - } - // Calculate DragImpact // at this point we know that the destination is droppable const destinationDisplacement: Displacement = { @@ -144,9 +130,35 @@ export default ({ direction: droppable.axis.direction, }; + const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ + draggable, + destination: droppable, + newPageCenter, + viewport, + }); + + if (isVisibleInNewLocation) { + return { + pageCenter: withDroppableScroll(droppable, newPageCenter), + impact: newImpact, + scrollJumpRequest: null, + }; + } + + // The full distance required to get from the previous page center to the new page center + const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); + + // We need to consider how much the droppable scroll has changed + const scrollDiff: Position = droppable.viewport.closestScrollable ? + droppable.viewport.closestScrollable.scroll.diff.value : + origin; + + // The actual scroll required to move into the next place + const requiredScroll: Position = subtract(requiredDistance, scrollDiff); + return { - pageCenter: withDroppableScroll(droppable, newPageCenter), + pageCenter: previousPageCenter, impact: newImpact, - scrollJumpRequest: null, + scrollJumpRequest: requiredScroll, }; }; diff --git a/src/state/reducer.js b/src/state/reducer.js index e9c7722442..0ef4382563 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -53,7 +53,7 @@ type MoveArgs = {| shouldAnimate: boolean, windowScroll ?: Position, // force a custom drag impact (optionally provided) - impact?: DragImpact, + impact?: ?DragImpact, // provide a scroll jump request (optionally provided - and can be null) scrollJumpRequest?: ?Position, |} @@ -131,7 +131,7 @@ const move = ({ }; }; -const updateStateAfterDimensionChange = (newState: State): State => { +const updateStateAfterDimensionChange = (newState: State, impact?: ?DragImpact): State => { // not dragging yet if (newState.phase === 'COLLECTING_INITIAL_DIMENSIONS') { return newState; @@ -148,19 +148,12 @@ const updateStateAfterDimensionChange = (newState: State): State => { return clean(); } - // If in JUMP auto scroll mode - then impacts are calculated before the scroll - // actually occurs - // const usePreviousImpact: boolean = newState.drag.initial.autoScrollMode === 'JUMP'; - - // if (usePreviousImpact) { - // console.log('USING PREVIOUS IMPACT'); - // } - return move({ state: newState, // use the existing values clientSelection: newState.drag.current.client.selection, shouldAnimate: newState.drag.current.shouldAnimate, + impact, }); }; @@ -386,6 +379,10 @@ export default (state: State = clean('IDLE'), action: Action): State => { const dimension: DroppableDimension = scrollDroppable(target, offset); + // If we are jump scrolling - dimension changes should not update the impact + const impact: ?DragImpact = drag.initial.autoScrollMode === 'JUMP' ? + drag.impact : null; + const newState: State = { ...state, dimension: { @@ -398,7 +395,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { }, }; - return updateStateAfterDimensionChange(newState); + return updateStateAfterDimensionChange(newState, impact); } if (action.type === 'UPDATE_DROPPABLE_DIMENSION_IS_ENABLED') { @@ -442,33 +439,51 @@ export default (state: State = clean('IDLE'), action: Action): State => { // TODO: finished initial collection? // Otherwise get an incorrect index calculated before the other dimensions are published const { client, windowScroll, shouldAnimate } = action.payload; + const drag: ?DragState = state.drag; + + if (!drag) { + console.error('Cannot move while there is no drag state'); + return state; + } + + // If we are jump scrolling - manual movements should not update the impact + const impact: ?DragImpact = drag.initial.autoScrollMode === 'JUMP' ? + drag.impact : null; + return move({ state, clientSelection: client, windowScroll, shouldAnimate, + impact, }); } if (action.type === 'MOVE_BY_WINDOW_SCROLL') { const { windowScroll } = action.payload; + const drag: ?DragState = state.drag; - if (!state.drag) { + if (!drag) { console.error('cannot move with window scrolling if no current drag'); return clean(); } - const current: CurrentDrag = state.drag.current; - - if (isEqual(windowScroll, current.windowScroll)) { + if (isEqual(windowScroll, drag.current.windowScroll)) { return state; } + // return state; + const isJumpScrolling: boolean = drag.initial.autoScrollMode === 'JUMP'; + + // If we are jump scrolling - any window scrolls should not update the impact + const impact: ?DragImpact = isJumpScrolling ? drag.impact : null; + return move({ state, - clientSelection: current.client.selection, + clientSelection: drag.current.client.selection, windowScroll, shouldAnimate: false, + impact, }); } diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js index 8792170087..0c36c5927c 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/announcer/announcer.js @@ -43,7 +43,7 @@ export default (): Announcer => { } el.textContent = message; - console.log('announcing:', message); + // console.log('announcing:', message); }; const mount = () => { diff --git a/stories/1-single-vertical-list-story.js b/stories/1-single-vertical-list-story.js index bc4a0281f2..a1d85300fc 100644 --- a/stories/1-single-vertical-list-story.js +++ b/stories/1-single-vertical-list-story.js @@ -7,8 +7,8 @@ import { quotes, getQuotes } from './src/data'; import { grid } from './src/constants'; const data = { - // small: quotes, - small: getQuotes(3), + small: quotes, + // small: getQuotes(3), medium: getQuotes(40), large: getQuotes(500), }; diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index 15f44ef6e3..4af5d62d4d 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -7,7 +7,7 @@ import QuoteList from '../primatives/quote-list'; import { colors, grid } from '../constants'; import reorder from '../reorder'; import type { Quote } from '../types'; -import type { DropResult, DragStart, DragUpdate, Announce } from '../../../src/types'; +import type { DropResult, DragStart } from '../../../src/types'; const publishOnDragStart = action('onDragStart'); const publishOnDragEnd = action('onDragEnd'); @@ -40,11 +40,7 @@ export default class QuoteApp extends Component { quotes: this.props.initial, }; - onDragStart = (initial: DragStart, announce: Announce) => { - announce(` - Item lifted. ${initial.source.index + 1} of ${this.state.quotes.length} in the list. - Use the arrow keys to move, space bar to drop, and escape to cancel. - `); + onDragStart = (initial: DragStart) => { publishOnDragStart(initial); // Add a little vibration if the browser supports it. // Add's a nice little physical feedback @@ -53,30 +49,7 @@ export default class QuoteApp extends Component { } } - onDragUpdate = (update: DragUpdate, announce: Announce) => { - if (!update.destination) { - announce('You are currently not dragging over any droppable area'); - return; - } - announce(`Now ${update.destination.index + 1} of ${this.state.quotes.length} in the list`); - } - - onDragEnd = (result: DropResult, announce: Announce) => { - if (result.reason === 'CANCEL') { - announce('drop cancelled'); - return; - } - - if (!result.destination) { - announce(` - Item has been dropped while not over a location. - It has been returned to its original position of ${result.source.index + 1} of ${this.state.quotes.length} - `); - return; - } - - announce(`Item dropped. It has moved from position ${result.source.index + 1} to ${result.destination.index + 1}`); - + onDragEnd = (result: DropResult) => { publishOnDragEnd(result); // dropped outside the list @@ -101,7 +74,6 @@ export default class QuoteApp extends Component { return ( From 15812e97b038ae3a09cb55441a14e65cb702a059 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 09:43:01 +1100 Subject: [PATCH 098/163] things are looking better --- .../get-scroll-jump-result.js | 44 ------------------- .../move-to-next-index/in-foreign-list.js | 32 +++++++------- src/state/move-to-next-index/in-home-list.js | 17 ++----- src/state/with-droppable-displacement.js | 4 +- src/state/with-droppable-scroll.js | 4 +- 5 files changed, 23 insertions(+), 78 deletions(-) delete mode 100644 src/state/move-to-next-index/get-scroll-jump-result.js diff --git a/src/state/move-to-next-index/get-scroll-jump-result.js b/src/state/move-to-next-index/get-scroll-jump-result.js deleted file mode 100644 index b01c4153c6..0000000000 --- a/src/state/move-to-next-index/get-scroll-jump-result.js +++ /dev/null @@ -1,44 +0,0 @@ -// @flow -import { subtract } from '../position'; -import type { Result } from './move-to-next-index-types'; -import type { - Position, - DroppableDimension, - DragImpact, -} from '../../types'; - -type Args = {| - newPageCenter: Position, - previousPageCenter: Position, - droppable: DroppableDimension, - previousImpact: DragImpact, -|} - -const origin: Position = { x: 0, y: 0 }; - -export default ({ - newPageCenter, - previousPageCenter, - droppable, - previousImpact, -}: Args): Result => { - // The full distance required to get from the previous page center to the new page center - const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); - - // We need to consider how much the droppable scroll has changed - const scrollDiff: Position = droppable.viewport.closestScrollable ? - droppable.viewport.closestScrollable.scroll.diff.value : - origin; - - // The actual scroll required to move into the next place - const requiredScroll: Position = subtract(requiredDistance, scrollDiff); - - return { - // Using the previous page center with a new impact - // as we are not visually moving the Draggable - pageCenter: previousPageCenter, - impact: previousImpact, - scrollJumpRequest: requiredScroll, - }; -}; - diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index 5486c3a1e1..745a8c3855 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -1,12 +1,11 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch } from '../position'; -import withDroppableScroll from '../with-droppable-scroll'; +import { patch, subtract } from '../position'; +import withDroppableDisplacement from '../with-droppable-displacement'; import moveToEdge from '../move-to-edge'; import getDisplacement from '../get-displacement'; import getViewport from '../../window/get-viewport'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; -import getScrollJumpResult from './get-scroll-jump-result'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import type { @@ -19,8 +18,6 @@ import type { Area, } from '../../types'; -const origin: Position = { x: 0, y: 0 }; - export default ({ isMovingForward, draggableId, @@ -93,15 +90,6 @@ export default ({ viewport, }); - if (!isVisibleInNewLocation) { - return getScrollJumpResult({ - newPageCenter, - previousPageCenter, - droppable, - previousImpact, - }); - } - // at this point we know that the destination is droppable const movingRelativeToDisplacement: Displacement = { draggableId: movingRelativeTo.descriptor.id, @@ -152,9 +140,21 @@ export default ({ direction: droppable.axis.direction, }; + if (isVisibleInNewLocation) { + return { + pageCenter: withDroppableDisplacement(droppable, newPageCenter), + impact: newImpact, + scrollJumpRequest: null, + }; + } + + // The full distance required to get from the previous page center to the new page center + const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); + const requiredScroll: Position = withDroppableDisplacement(droppable, requiredDistance); + return { - pageCenter: withDroppableScroll(droppable, newPageCenter), + pageCenter: previousPageCenter, impact: newImpact, - scrollJumpRequest: null, + scrollJumpRequest: requiredScroll, }; }; diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 8d5ec4d708..a178c1865c 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,14 +1,14 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import { patch, subtract } from '../position'; -import withDroppableScroll from '../with-droppable-scroll'; +import withDroppableDisplacement from '../with-droppable-displacement'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import getViewport from '../../window/get-viewport'; // import getScrollJumpResult from './get-scroll-jump-result'; import moveToEdge from '../move-to-edge'; +import getDisplacement from '../get-displacement'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; -import getDisplacement from '../get-displacement'; import type { DraggableLocation, DraggableDimension, @@ -19,8 +19,6 @@ import type { Area, } from '../../types'; -const origin: Position = { x: 0, y: 0 }; - export default ({ isMovingForward, draggableId, @@ -139,7 +137,7 @@ export default ({ if (isVisibleInNewLocation) { return { - pageCenter: withDroppableScroll(droppable, newPageCenter), + pageCenter: withDroppableDisplacement(droppable, newPageCenter), impact: newImpact, scrollJumpRequest: null, }; @@ -147,14 +145,7 @@ export default ({ // The full distance required to get from the previous page center to the new page center const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); - - // We need to consider how much the droppable scroll has changed - const scrollDiff: Position = droppable.viewport.closestScrollable ? - droppable.viewport.closestScrollable.scroll.diff.value : - origin; - - // The actual scroll required to move into the next place - const requiredScroll: Position = subtract(requiredDistance, scrollDiff); + const requiredScroll: Position = withDroppableDisplacement(droppable, requiredDistance); return { pageCenter: previousPageCenter, diff --git a/src/state/with-droppable-displacement.js b/src/state/with-droppable-displacement.js index ee3373a8e1..384f354864 100644 --- a/src/state/with-droppable-displacement.js +++ b/src/state/with-droppable-displacement.js @@ -6,12 +6,10 @@ import type { DroppableDimension, } from '../types'; -const origin: Position = { x: 0, y: 0 }; - export default (droppable: DroppableDimension, point: Position): Position => { const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; if (!closestScrollable) { - return origin; + return point; } return add(point, closestScrollable.scroll.diff.displacement); diff --git a/src/state/with-droppable-scroll.js b/src/state/with-droppable-scroll.js index e17734c2bf..5f3292f31a 100644 --- a/src/state/with-droppable-scroll.js +++ b/src/state/with-droppable-scroll.js @@ -1,5 +1,5 @@ // @flow -import { subtract } from './position'; +import { add } from './position'; import type { Position, ClosestScrollable, @@ -12,5 +12,5 @@ export default (droppable: DroppableDimension, point: Position): Position => { return point; } - return subtract(point, closestScrollable.scroll.diff.value); + return add(point, closestScrollable.scroll.diff.value); }; From 88710b10989cdec5a20b2314dda3feab418a5ef7 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 09:52:57 +1100 Subject: [PATCH 099/163] streamlining scroll displacement calcs --- .../move-to-new-droppable/to-foreign-list.js | 11 ++--------- .../move-to-new-droppable/to-home-list.js | 6 ++---- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js index 318eaa570a..65c5604933 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js +++ b/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js @@ -3,7 +3,7 @@ import moveToEdge from '../../move-to-edge'; import type { Result } from '../move-cross-axis-types'; import getDisplacement from '../../get-displacement'; import getViewport from '../../../window/get-viewport'; -import { add } from '../../position'; +import withDroppableDisplacement from '../../with-droppable-displacement'; import type { Axis, Position, @@ -14,8 +14,6 @@ import type { Displacement, } from '../../../types'; -const origin: Position = { x: 0, y: 0 }; - type Args = {| amount: Position, pageCenter: Position, @@ -118,13 +116,8 @@ export default ({ }, }; - const scrollDisplacement: Position = droppable.viewport.closestScrollable ? - droppable.viewport.closestScrollable.scroll.diff.displacement : - origin; - const withDisplacement: Position = add(newCenter, scrollDisplacement); - return { - pageCenter: withDisplacement, + pageCenter: withDroppableDisplacement(droppable, newCenter), impact: newImpact, }; }; diff --git a/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js index 4ec809d17c..36ecf0b68a 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js +++ b/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js @@ -2,7 +2,7 @@ import moveToEdge from '../../move-to-edge'; import getViewport from '../../../window/get-viewport'; import getDisplacement from '../../get-displacement'; -import withDroppableScroll from '../../with-droppable-scroll'; +import withDroppableDisplacement from '../../with-droppable-displacement'; import type { Edge } from '../../move-to-edge'; import type { Result } from '../move-cross-axis-types'; import type { @@ -25,8 +25,6 @@ type Args = {| previousImpact: DragImpact, |} -const origin: Position = { x: 0, y: 0 }; - export default ({ amount, originalIndex, @@ -131,7 +129,7 @@ export default ({ }; return { - pageCenter: withDroppableScroll(droppable, newCenter), + pageCenter: withDroppableDisplacement(droppable, newCenter), impact: newImpact, }; }; From f581db30df02ef7ea0fe2443c940fe7af8713264 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 10:27:00 +1100 Subject: [PATCH 100/163] adding todo --- src/state/move-cross-axis/get-best-cross-axis-droppable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/state/move-cross-axis/get-best-cross-axis-droppable.js b/src/state/move-cross-axis/get-best-cross-axis-droppable.js index 7abc23da71..3e5b9e1952 100644 --- a/src/state/move-cross-axis/get-best-cross-axis-droppable.js +++ b/src/state/move-cross-axis/get-best-cross-axis-droppable.js @@ -65,6 +65,7 @@ export default ({ if (!clipped) { return false; } + // TODO: only need to be totally visible on the cross axis return isPartiallyVisibleThroughFrame(viewport)(clipped); }) .filter((droppable: DroppableDimension): boolean => { From ddde53e6da7951d26df78ff69efe6d062522226c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 15:15:18 +1100 Subject: [PATCH 101/163] new api. fixing hook tests --- src/state/hooks/hook-caller.js | 139 +++++++++------- src/state/hooks/message-preset.js | 23 ++- src/view/draggable/connected-draggable.js | 23 ++- src/view/draggable/draggable-types.js | 7 +- src/view/draggable/draggable.jsx | 16 +- src/view/droppable/connected-droppable.js | 17 +- src/view/droppable/droppable-types.js | 4 + src/view/droppable/droppable.jsx | 2 + stories/src/primatives/quote-item.jsx | 4 +- test/unit/state/hook-caller.spec.js | 193 ++++++++++++++++++---- 10 files changed, 323 insertions(+), 105 deletions(-) diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index 5d52a477f5..aecc0064ff 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -22,7 +22,7 @@ type State = { hasMovedFromStartLocation: boolean, } -const initial: State = { +const notDragging: State = { isDragging: false, start: null, lastDestination: null, @@ -45,19 +45,43 @@ const areLocationsEqual = (current: ?DraggableLocation, next: ?DraggableLocation current.index === next.index; }; -const getProvided = (announce: Announce): HookProvided => { - const detector = (message: string) => { +const getAnnouncerForConsumer = (announce: Announce) => { + let wasCalled: boolean = false; + let isExpired: boolean = false; + + // not allowing async announcements + setTimeout(() => { + isExpired = true; + }); + + const result = (message: string): void => { + if (wasCalled) { + console.warn('Announcement already made. Not making a second announcement'); + return; + } + + if (isExpired) { + console.warn(` + Announcements cannot be made asynchronously. + Default message has already been announced. + `); + return; + } + + wasCalled = true; announce(message); - detector.wasCalled = true; }; - return { - announce: detector, - }; + // getter for isExpired + result.wasCalled = (): boolean => wasCalled; + + return result; }; +type OnDragUpdate = (update: DragUpdate, provided: HookProvided) => void; + export default (announce: Announce): HookCaller => { - let state: State = initial; + let state: State = notDragging; const setState = (partial: Object): void => { const newState: State = { @@ -93,35 +117,38 @@ export default (announce: Announce): HookCaller => { return start; }; - const onStateChange = (hooks: Hooks, previous: AppState, current: AppState): void => { - const { onDragStart, onDragUpdate, onDragEnd } = hooks; - const currentPhase = current.phase; - const previousPhase = previous.phase; - - // Dragging in progress - if (currentPhase === 'DRAGGING' && previousPhase === 'DRAGGING') { - if (!state.isDragging) { - console.error('Cannot process dragging update if drag has not started'); + const onDrag = (() => { + const announceMessage = (update: DragUpdate, onDragUpdate: ?OnDragUpdate) => { + if (!onDragUpdate) { + announce(messagePreset.onDragUpdate(update)); return; } - // only call the onDragUpdate hook if something has changed from last time - const start: ?DragStart = getDragStart(current); + const provided: HookProvided = { + announce: getAnnouncerForConsumer(announce), + }; + onDragUpdate(update, provided); - if (!start) { - console.error('Cannot update drag when there is invalid state'); + // if they do not announce - use the default + if (!provided.announce.wasCalled()) { + announce(messagePreset.onDragUpdate(update)); + } + }; + + return (current: AppState, onDragUpdate?: OnDragUpdate) => { + if (!state.isDragging) { + console.error('Cannot process dragging update if drag has not started'); return; } const drag: ?DragState = current.drag; - - if (!drag) { + const start: ?DragStart = getDragStart(current); + if (!start || !drag) { console.error('Cannot update drag when there is invalid state'); return; } const destination: ?DraggableLocation = drag.impact.destination; - const update: DragUpdate = { draggableId: start.draggableId, type: start.type, @@ -129,31 +156,19 @@ export default (announce: Announce): HookCaller => { destination, }; - // has not left the home position if (!state.hasMovedFromStartLocation) { // has not moved past the home yet if (areLocationsEqual(start.source, destination)) { return; } + // We have now moved past the home location setState({ lastDestination: destination, hasMovedFromStartLocation: true, }); - if (!onDragUpdate) { - announce(messagePreset.onDragUpdate(update)); - return; - } - - const provided: HookProvided = getProvided(announce); - onDragUpdate(update, provided); - - // if they do not announce - use the default - if (!provided.announce.wasCalled) { - announce(messagePreset.onDragUpdate(update)); - } - + announceMessage(update, onDragUpdate); return; } @@ -166,25 +181,24 @@ export default (announce: Announce): HookCaller => { lastDestination: destination, }); - if (!onDragUpdate) { - announce(messagePreset.onDragUpdate(update)); - return; - } - - const provided: HookProvided = getProvided(announce); - onDragUpdate(update, provided); + announceMessage(update, onDragUpdate); + }; + })(); - // if they did not announce anything - use the default - if (!provided.announce.wasCalled) { - announce(messagePreset.onDragUpdate(update)); - } + const onStateChange = (hooks: Hooks, previous: AppState, current: AppState): void => { + const { onDragStart, onDragUpdate, onDragEnd } = hooks; + const currentPhase = current.phase; + const previousPhase = previous.phase; + // Dragging in progress + if (currentPhase === 'DRAGGING' && previousPhase === 'DRAGGING') { + onDrag(current, onDragUpdate); return; } // We are not in the dragging phase so we can clear this state if (state.isDragging) { - setState(initial); + setState(notDragging); } // From this point we only care about phase changes @@ -214,11 +228,13 @@ export default (announce: Announce): HookCaller => { return; } - const provided: HookProvided = getProvided(announce); + const provided: HookProvided = { + announce: getAnnouncerForConsumer(announce), + }; onDragStart(start, provided); // if they did not announce anything - use the default - if (!provided.announce.wasCalled) { + if (!provided.announce.wasCalled()) { announce(messagePreset.onDragStart(start)); } return; @@ -232,10 +248,12 @@ export default (announce: Announce): HookCaller => { } const result: DropResult = current.drop.result; - const provided: HookProvided = getProvided(announce); + const provided: HookProvided = { + announce: getAnnouncerForConsumer(announce), + }; onDragEnd(result, provided); - if (!provided.announce.wasCalled) { + if (!provided.announce.wasCalled()) { announce(messagePreset.onDragEnd(result)); } return; @@ -268,10 +286,12 @@ export default (announce: Announce): HookCaller => { reason: 'CANCEL', }; - const provided: HookProvided = getProvided(announce); + const provided: HookProvided = { + announce: getAnnouncerForConsumer(announce), + }; onDragEnd(result, provided); - if (!provided.announce.wasCalled) { + if (!provided.announce.wasCalled()) { announce(messagePreset.onDragEnd(result)); } @@ -293,10 +313,13 @@ export default (announce: Announce): HookCaller => { destination: null, reason: 'CANCEL', }; - const provided: HookProvided = getProvided(announce); + + const provided: HookProvided = { + announce: getAnnouncerForConsumer(announce), + }; onDragEnd(result, provided); - if (!provided.announce.wasCalled) { + if (!provided.announce.wasCalled()) { announce(messagePreset.onDragEnd(result)); } } diff --git a/src/state/hooks/message-preset.js b/src/state/hooks/message-preset.js index 6f8108b1c7..7e0d557e42 100644 --- a/src/state/hooks/message-preset.js +++ b/src/state/hooks/message-preset.js @@ -11,6 +11,8 @@ export type MessagePreset = {| onDragEnd: (result: DropResult) => string, |} +// We cannot list what index the Droppable is in automatically as we are not sure how +// the Droppable's have been configured const onDragStart = (start: DragStart): string => ` You have lifted an item in position ${start.source.index + 1}. Use the arrow keys to move, space bar to drop, and escape to cancel. @@ -20,8 +22,18 @@ const onDragUpdate = (update: DragUpdate): string => { if (!update.destination) { return 'You are currently not dragging over any droppable area'; } - // TODO: list what droppable they are in? - return `You have moved the item to position ${update.destination.index + 1}`; + + // Moving in the same list + if (update.source.droppableId === update.destination.droppableId) { + return `You have moved the item to position ${update.destination.index + 1}`; + } + + // Moving into a new list + + return ` + You have moved the item from list ${update.source.droppableId} in position ${update.source.index + 1} + to list ${update.destination.droppableId} in position ${update.destination.index + 1} + `; }; const onDragEnd = (result: DropResult): string => { @@ -32,13 +44,15 @@ const onDragEnd = (result: DropResult): string => { `; } + // Not moved anywhere (such as when dropped over no list) if (!result.destination) { return ` - The item has been dropped while not over a location. + The item has been dropped while not over a droppable location. The item has returned to its starting position of ${result.source.index + 1} `; } + // Dropped in home list if (result.source.droppableId === result.destination.droppableId) { return ` You have dropped the item. @@ -46,8 +60,9 @@ const onDragEnd = (result: DropResult): string => { `; } + // Dropped in a new list return ` - Item dropped. + You have dropped the item. It has moved from position ${result.source.index + 1} in list ${result.source.droppableId} to position ${result.destination.index + 1} in list ${result.destination.droppableId} `; diff --git a/src/view/draggable/connected-draggable.js b/src/view/draggable/connected-draggable.js index 041573a23f..67246e55e4 100644 --- a/src/view/draggable/connected-draggable.js +++ b/src/view/draggable/connected-draggable.js @@ -22,10 +22,12 @@ import type { State, Position, DraggableId, + DroppableId, DragMovement, DraggableDimension, Direction, Displacement, + PendingDrop, } from '../../types'; import type { MapProps, @@ -47,6 +49,7 @@ const defaultMapProps: MapProps = { // these properties are only populated when the item is dragging dimension: null, direction: null, + draggingOver: null, }; export const makeSelector = (): Selector => { @@ -66,6 +69,7 @@ export const makeSelector = (): Selector => { shouldAnimateDragMovement: false, dimension: null, direction: null, + draggingOver: null, }), ); @@ -75,6 +79,8 @@ export const makeSelector = (): Selector => { dimension: DraggableDimension, // direction of the droppable you are over direction: ?Direction, + // the id of the droppable you are over + draggingOver: ?DroppableId, ): MapProps => ({ isDragging: true, isDropAnimating: false, @@ -83,6 +89,7 @@ export const makeSelector = (): Selector => { shouldAnimateDragMovement, dimension, direction, + draggingOver, })); const draggingSelector = (state: State, ownProps: OwnProps): ?MapProps => { @@ -105,34 +112,44 @@ export const makeSelector = (): Selector => { const dimension: DraggableDimension = state.dimension.draggable[ownProps.draggableId]; const direction: ?Direction = state.drag.impact.direction; const shouldAnimateDragMovement: boolean = state.drag.current.shouldAnimate; + const draggingOver: ?DroppableId = state.drag.impact.destination ? + state.drag.impact.destination.droppableId : + null; return getDraggingProps( memoizedOffset(offset.x, offset.y), shouldAnimateDragMovement, dimension, direction, + draggingOver, ); } // dropping - if (!state.drop || !state.drop.pending) { + const pending: ?PendingDrop = state.drop && state.drop.pending; + + if (!pending) { console.error('cannot provide props for dropping item when there is invalid state'); return null; } // this was not the dragging item - if (state.drop.pending.result.draggableId !== ownProps.draggableId) { + if (pending.result.draggableId !== ownProps.draggableId) { return null; } + const draggingOver: ?DroppableId = pending.result.destination ? + pending.result.destination.droppableId : null; + // not memoized as it is the only execution return { isDragging: false, isDropAnimating: true, - offset: state.drop.pending.newHomeOffset, + offset: pending.newHomeOffset, // still need to provide the dimension for the placeholder dimension: state.dimension.draggable[ownProps.draggableId], + draggingOver, // direction no longer needed as drag handle is unbound direction: null, // animation will be controlled by the isDropAnimating flag diff --git a/src/view/draggable/draggable-types.js b/src/view/draggable/draggable-types.js index 95b59a7f10..36d765191d 100644 --- a/src/view/draggable/draggable-types.js +++ b/src/view/draggable/draggable-types.js @@ -2,6 +2,7 @@ import type { Node } from 'react'; import type { DraggableId, + DroppableId, DraggableDimension, Position, Direction, @@ -106,6 +107,7 @@ export type Provided = {| export type StateSnapshot = {| isDragging: boolean, + draggingOver: ?DroppableId, |} export type DispatchProps = {| @@ -129,12 +131,13 @@ export type MapProps = {| // when an item is being displaced by a dragging item, // we need to know if that movement should be animated shouldAnimateDisplacement: boolean, + isDropAnimating: boolean, + offset: Position, // only provided when dragging // can be null if not over a droppable direction: ?Direction, - isDropAnimating: boolean, - offset: Position, dimension: ?DraggableDimension, + draggingOver: ?DroppableId, |} export type OwnProps = {| diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 4004d07305..6e59bca416 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -275,8 +275,13 @@ export default class Draggable extends Component { } ) - getSnapshot = memoizeOne((isDragging: boolean, isDropAnimating: boolean): StateSnapshot => ({ + getSnapshot = memoizeOne(( + isDragging: boolean, + isDropAnimating: boolean, + draggingOver: ?DroppableId, + ): StateSnapshot => ({ isDragging: (isDragging || isDropAnimating), + draggingOver, })) getSpeed = memoizeOne( @@ -303,11 +308,12 @@ export default class Draggable extends Component { isDropAnimating, isDragDisabled, dimension, - children, + draggingOver, direction, shouldAnimateDragMovement, shouldAnimateDisplacement, disableInteractiveElementBlocking, + children, } = this.props; const droppableId: DroppableId = this.context[droppableIdKey]; @@ -350,7 +356,11 @@ export default class Draggable extends Component { dragHandleProps, movementStyle, ), - this.getSnapshot(isDragging, isDropAnimating) + this.getSnapshot( + isDragging, + isDropAnimating, + draggingOver, + ) ) } diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index ef7bd582d3..08810b8f40 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -16,6 +16,7 @@ import type { DragState, State, DroppableId, + DraggableId, DraggableLocation, DraggableDimension, Placeholder, @@ -71,8 +72,12 @@ export const makeSelector = (): Selector => { ); const getMapProps = memoizeOne( - (isDraggingOver: boolean, placeholder: ?Placeholder): MapProps => ({ + (isDraggingOver: boolean, + draggingOverWith: ?DraggableId, + placeholder: ?Placeholder, + ): MapProps => ({ isDraggingOver, + draggingOverWith, placeholder, }) ); @@ -103,13 +108,16 @@ export const makeSelector = (): Selector => { } const isDraggingOver = getIsDraggingOver(id, drag.impact.destination); + const draggingOverWith: ?DraggableId = isDraggingOver ? + drag.initial.descriptor.id : null; const placeholder: ?Placeholder = getPlaceholder( id, drag.impact.destination, draggable, ); - return getMapProps(isDraggingOver, placeholder); + + return getMapProps(isDraggingOver, draggingOverWith, placeholder); } if (phase === 'DROP_ANIMATING') { @@ -119,12 +127,15 @@ export const makeSelector = (): Selector => { } const isDraggingOver = getIsDraggingOver(id, pending.impact.destination); + const draggingOverWith: ?DraggableId = isDraggingOver ? + pending.result.draggableId : null; + const placeholder: ?Placeholder = getPlaceholder( id, pending.result.destination, draggable, ); - return getMapProps(isDraggingOver, placeholder); + return getMapProps(isDraggingOver, draggingOverWith, placeholder); } return getMapProps(false, null); diff --git a/src/view/droppable/droppable-types.js b/src/view/droppable/droppable-types.js index 0e11225dca..0bb3412e3f 100644 --- a/src/view/droppable/droppable-types.js +++ b/src/view/droppable/droppable-types.js @@ -1,6 +1,7 @@ // @flow import type { Node } from 'react'; import type { + DraggableId, DroppableId, TypeId, Direction, @@ -20,10 +21,13 @@ export type Provided = {| export type StateSnapshot = {| isDraggingOver: boolean, + draggingOverWith: ?DraggableId, |} export type MapProps = {| isDraggingOver: boolean, + // The id of the draggable that is dragging over + draggingOverWith: ?DraggableId, // placeholder is used to hold space when // not the user is dragging over a list that // is not the source list diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 8be049cd9f..9f4f5ffe94 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -95,6 +95,7 @@ export default class Droppable extends Component { ignoreContainerClipping, isDraggingOver, isDropDisabled, + draggingOverWith, type, } = this.props; const provided: Provided = { @@ -106,6 +107,7 @@ export default class Droppable extends Component { }; const snapshot: StateSnapshot = { isDraggingOver, + draggingOverWith, }; return ( diff --git a/stories/src/primatives/quote-item.jsx b/stories/src/primatives/quote-item.jsx index 71a268f6c9..1efdce758d 100644 --- a/stories/src/primatives/quote-item.jsx +++ b/stories/src/primatives/quote-item.jsx @@ -13,8 +13,6 @@ type Props = { autoFocus?: boolean, } -type HTMLElement = any; - const Container = styled.a` border-radius: ${borderRadius}px; border: 1px solid grey; @@ -107,7 +105,7 @@ export default class QuoteItem extends React.PureComponent { } // eslint-disable-next-line react/no-find-dom-node - const node: HTMLElement = ReactDOM.findDOMNode(this); + const node: HTMLElement = (ReactDOM.findDOMNode(this) : any); node.focus(); } diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index 038922a464..548eacbaa5 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -1,5 +1,6 @@ // @flow import createHookCaller from '../../../src/state/hooks/hook-caller'; +import messagePreset from '../../../src/state/hooks/message-preset'; import type { HookCaller } from '../../../src/state/hooks/hooks-types'; import * as state from '../../utils/simple-state-preset'; import { getPreset } from '../../utils/dimension'; @@ -7,10 +8,13 @@ import noImpact, { noMovement } from '../../../src/state/no-impact'; import type { Announce, Hooks, + HookProvided, DropResult, State, DimensionState, DraggableLocation, + DraggableDescriptor, + DroppableDimension, DragStart, DragUpdate, DragImpact, @@ -27,9 +31,11 @@ const noDimensions: DimensionState = { describe('fire hooks', () => { let hooks: Hooks; let caller: HookCaller; - const announceMock: Announce = () => { }; + let announceMock: Announce; beforeEach(() => { + jest.useFakeTimers(); + announceMock = jest.fn(); caller = createHookCaller(announceMock); hooks = { onDragStart: jest.fn(), @@ -37,10 +43,13 @@ describe('fire hooks', () => { onDragEnd: jest.fn(), }; jest.spyOn(console, 'error').mockImplementation(() => { }); + jest.spyOn(console, 'warn').mockImplementation(() => { }); }); afterEach(() => { console.error.mockRestore(); + console.warn.mockRestore(); + jest.useRealTimers(); }); describe('drag start', () => { @@ -56,17 +65,9 @@ describe('fire hooks', () => { caller.onStateChange(hooks, state.requesting(), state.dragging()); - expect(hooks.onDragStart).toHaveBeenCalledWith(expected, announceMock); - }); - - it('should do nothing if no onDragStart is not provided', () => { - const customHooks: Hooks = { - onDragEnd: jest.fn(), - }; - - caller.onStateChange(customHooks, state.requesting(), state.dragging()); - - expect(console.error).not.toHaveBeenCalled(); + expect(hooks.onDragStart).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); it('should log an error and not call the callback if there is no current drag', () => { @@ -86,6 +87,114 @@ describe('fire hooks', () => { expect(hooks.onDragStart).not.toHaveBeenCalled(); }); + + describe('announcements', () => { + const getDragStart = (appState: State): ?DragStart => { + if (!appState.drag) { + return null; + } + + const descriptor: DraggableDescriptor = appState.drag.initial.descriptor; + const home: ?DroppableDimension = appState.dimension.droppable[descriptor.droppableId]; + + if (!home) { + return null; + } + + const source: DraggableLocation = { + index: descriptor.index, + droppableId: descriptor.droppableId, + }; + + const start: DragStart = { + draggableId: descriptor.id, + type: home.descriptor.type, + source, + }; + + return start; + }; + + const dragStart: ?DragStart = getDragStart(state.dragging()); + if (!dragStart) { + throw new Error('Invalid test setup'); + } + + it('should announce with the default lift message if no message is provided', () => { + caller.onStateChange(hooks, state.requesting(), state.dragging()); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragStart(dragStart)); + }); + + it('should announce with the default lift message if no onDragStart hook is provided', () => { + const customHooks: Hooks = { + onDragEnd: jest.fn(), + }; + + caller.onStateChange(customHooks, state.requesting(), state.dragging()); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragStart(dragStart)); + }); + + it('should announce with a provided message', () => { + const customHooks: Hooks = { + onDragStart: (start: DragStart, provided: HookProvided) => provided.announce('test'), + onDragEnd: jest.fn(), + }; + + caller.onStateChange(customHooks, state.requesting(), state.dragging()); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(announceMock).toHaveBeenCalledWith('test'); + }); + + it('should prevent double announcing', () => { + let myAnnounce: ?Announce; + const customHooks: Hooks = { + onDragStart: (start: DragStart, provided: HookProvided) => { + myAnnounce = provided.announce; + myAnnounce('test'); + }, + onDragEnd: jest.fn(), + }; + + caller.onStateChange(customHooks, state.requesting(), state.dragging()); + expect(announceMock).toHaveBeenCalledWith('test'); + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).not.toHaveBeenCalled(); + + if (!myAnnounce) { + throw new Error('Invalid test setup'); + } + + myAnnounce('second'); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); + + it('should prevent async announcing', () => { + const customHooks: Hooks = { + onDragStart: (start: DragStart, provided: HookProvided) => { + setTimeout(() => { + // boom + provided.announce('too late'); + }); + }, + onDragEnd: jest.fn(), + }; + + caller.onStateChange(customHooks, state.requesting(), state.dragging()); + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragStart(dragStart)); + expect(console.warn).not.toHaveBeenCalled(); + + // not releasing the async message + jest.runOnlyPendingTimers(); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); + }); }); describe('drag update', () => { @@ -163,7 +272,9 @@ describe('fire hooks', () => { withImpact(state.dragging(), impact), ); - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); it('should provide an update if the droppable changes', () => { @@ -192,7 +303,9 @@ describe('fire hooks', () => { withImpact(state.dragging(), impact), ); - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); it('should provide an update if moving from a droppable to nothing', () => { @@ -210,7 +323,9 @@ describe('fire hooks', () => { withImpact(state.dragging(), noImpact), ); - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); }); @@ -283,7 +398,9 @@ describe('fire hooks', () => { withImpact(state.dragging(), secondImpact), ); - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); it('should provide an update if the droppable changes', () => { @@ -310,7 +427,9 @@ describe('fire hooks', () => { withImpact(state.dragging(), secondImpact), ); - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); it('should provide an update if moving from a droppable to nothing', () => { @@ -333,7 +452,9 @@ describe('fire hooks', () => { withImpact(state.dragging(), secondImpact), ); - expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, announceMock); + expect(hooks.onDragUpdate).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); it('should provide an update if moving back to the home location', () => { @@ -356,7 +477,9 @@ describe('fire hooks', () => { destination: null, }; - expect(hooks.onDragUpdate).toHaveBeenCalledWith(first, announceMock); + expect(hooks.onDragUpdate).toHaveBeenCalledWith(first, { + announce: expect.any(Function), + }); // drag back to home caller.onStateChange( @@ -370,7 +493,9 @@ describe('fire hooks', () => { source: start.source, destination: start.source, }; - expect(hooks.onDragUpdate).toHaveBeenCalledWith(second, announceMock); + expect(hooks.onDragUpdate).toHaveBeenCalledWith(second, { + announce: expect.any(Function), + }); }); }); @@ -408,7 +533,7 @@ describe('fire hooks', () => { type: start.type, source: start.source, destination: firstImpact.destination, - }, announceMock); + }, { announce: expect.any(Function) }); // second move into new location const secondImpact: DragImpact = { @@ -432,7 +557,7 @@ describe('fire hooks', () => { type: start.type, source: start.source, destination: secondImpact.destination, - }, announceMock); + }, { announce: expect.any(Function) }); }); it('should update correctly across multiple drags', () => { @@ -467,7 +592,7 @@ describe('fire hooks', () => { type: start.type, source: start.source, destination: firstImpact.destination, - }, announceMock); + }, { announce: expect.any(Function) }); // resetting the mock // $ExpectError - resetting mock hooks.onDragUpdate.mockReset(); @@ -503,7 +628,7 @@ describe('fire hooks', () => { type: start.type, source: start.source, destination: firstImpact.destination, - }, announceMock); + }, { announce: expect.any(Function) }); }); }); }); @@ -548,7 +673,9 @@ describe('fire hooks', () => { } const provided: DropResult = current.drop.result; - expect(hooks.onDragEnd).toHaveBeenCalledWith(provided, announceMock); + expect(hooks.onDragEnd).toHaveBeenCalledWith(provided, { + announce: expect.any(Function), + }); }); it('should log an error and not call the callback if there is no drop result', () => { @@ -586,7 +713,9 @@ describe('fire hooks', () => { caller.onStateChange(hooks, previous, current); - expect(hooks.onDragEnd).toHaveBeenCalledWith(result, announceMock); + expect(hooks.onDragEnd).toHaveBeenCalledWith(result, { + announce: expect.any(Function), + }); }); it('should call onDragEnd with original source if the item did not move', () => { @@ -621,7 +750,9 @@ describe('fire hooks', () => { caller.onStateChange(hooks, previous, current); - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, announceMock); + expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); }); }); @@ -643,7 +774,9 @@ describe('fire hooks', () => { caller.onStateChange(hooks, state.dragging(), state.idle); - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, announceMock); + expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); it('should log an error and do nothing if it cannot find a previous drag to publish', () => { @@ -677,7 +810,9 @@ describe('fire hooks', () => { caller.onStateChange(hooks, state.dropAnimating(), state.idle); - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, announceMock); + expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); it('should log an error and do nothing if it cannot find a previous drag to publish', () => { From 8f7a9583f740eb024d90d567f0b2255cce06f845 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 15:50:01 +1100 Subject: [PATCH 102/163] more annoucement tests --- test/unit/state/hook-caller.spec.js | 110 ++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index 548eacbaa5..d6eed6d384 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -327,6 +327,116 @@ describe('fire hooks', () => { announce: expect.any(Function), }); }); + + describe('announcements', () => { + const destination: DraggableLocation = { + // new index + index: preset.inHome1.descriptor.index + 1, + // different droppable + droppableId: preset.inHome1.descriptor.droppableId, + }; + const updateImpact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + destination, + }; + const dragUpdate: DragUpdate = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination, + }; + const inHome = withImpact(state.dragging(), inHomeImpact); + const withUpdate = withImpact(state.dragging(), updateImpact); + + const perform = (myHooks: Hooks) => { + caller.onStateChange(myHooks, inHome, withUpdate); + }; + + beforeEach(() => { + // from the lift + expect(announceMock).toHaveBeenCalledTimes(1); + // clear its state + announceMock.mockReset(); + }); + + it('should announce with the default update message if no message is provided', () => { + caller.onStateChange(hooks, inHome, withUpdate); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(dragUpdate)); + }); + + it('should announce with the default update message if no onDragUpdate hook is provided', () => { + const customHooks: Hooks = { + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(dragUpdate)); + }); + + it('should announce with a provided message', () => { + const customHooks: Hooks = { + onDragUpdate: (update: DragUpdate, provided: HookProvided) => provided.announce('test'), + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(announceMock).toHaveBeenCalledWith('test'); + }); + + it('should prevent double announcing', () => { + let myAnnounce: ?Announce; + const customHooks: Hooks = { + onDragUpdate: (update: DragUpdate, provided: HookProvided) => { + myAnnounce = provided.announce; + myAnnounce('test'); + }, + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith('test'); + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).not.toHaveBeenCalled(); + + if (!myAnnounce) { + throw new Error('Invalid test setup'); + } + + myAnnounce('second'); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); + + it('should prevent async announcing', () => { + const customHooks: Hooks = { + onDragUpdate: (update: DragUpdate, provided: HookProvided) => { + setTimeout(() => { + // boom + provided.announce('too late'); + }); + }, + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(dragUpdate)); + expect(console.warn).not.toHaveBeenCalled(); + + // not releasing the async message + jest.runOnlyPendingTimers(); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); + }); }); describe('no longer in home location', () => { From b84c34af61edd9cb62c2b8519adf1cd3f2fa8a6f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 15:57:37 +1100 Subject: [PATCH 103/163] more annoucement tests --- test/unit/state/hook-caller.spec.js | 109 ++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index d6eed6d384..c1280468b9 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -469,6 +469,7 @@ describe('fire hooks', () => { ); expect(hooks.onDragUpdate).toHaveBeenCalled(); + // cleaning the hook // $ExpectError - no mock reset property hooks.onDragUpdate.mockReset(); }); @@ -607,6 +608,114 @@ describe('fire hooks', () => { announce: expect.any(Function), }); }); + + describe('announcements', () => { + const destination: DraggableLocation = { + // new index + index: preset.inHome1.descriptor.index + 2, + // different droppable + droppableId: preset.inHome1.descriptor.droppableId, + }; + const secondImpact: DragImpact = { + movement: noMovement, + direction: preset.home.axis.direction, + destination, + }; + const secondUpdate: DragUpdate = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination, + }; + const withFirstUpdate = withImpact(state.dragging(), firstImpact); + const withSecondUpdate = withImpact(state.dragging(), secondImpact); + + const perform = (myHooks: Hooks) => { + caller.onStateChange(myHooks, withFirstUpdate, withSecondUpdate); + }; + + beforeEach(() => { + // clear its state from previous updates + announceMock.mockReset(); + }); + + it('should announce with the default update message if no message is provided', () => { + caller.onStateChange(hooks, withFirstUpdate, withSecondUpdate); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(secondUpdate)); + }); + + it('should announce with the default update message if no onDragUpdate hook is provided', () => { + const customHooks: Hooks = { + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(secondUpdate)); + }); + + it('should announce with a provided message', () => { + const customHooks: Hooks = { + onDragUpdate: (update: DragUpdate, provided: HookProvided) => provided.announce('test'), + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(announceMock).toHaveBeenCalledWith('test'); + }); + + it('should prevent double announcing', () => { + let myAnnounce: ?Announce; + const customHooks: Hooks = { + onDragUpdate: (update: DragUpdate, provided: HookProvided) => { + myAnnounce = provided.announce; + myAnnounce('test'); + }, + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith('test'); + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).not.toHaveBeenCalled(); + + if (!myAnnounce) { + throw new Error('Invalid test setup'); + } + + myAnnounce('second'); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); + + it('should prevent async announcing', () => { + const customHooks: Hooks = { + onDragUpdate: (update: DragUpdate, provided: HookProvided) => { + setTimeout(() => { + // boom + provided.announce('too late'); + }); + }, + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragUpdate(secondUpdate)); + expect(console.warn).not.toHaveBeenCalled(); + + // not releasing the async message + jest.runOnlyPendingTimers(); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); + }); }); describe('multiple updates', () => { From 00592f19b6d7862b9fb1b0fbc0f1a42f9048d9bf Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 16:19:48 +1100 Subject: [PATCH 104/163] more announcements checks --- test/unit/state/hook-caller.spec.js | 301 +++++++++++++++++++--------- 1 file changed, 206 insertions(+), 95 deletions(-) diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index c1280468b9..8ff17492b0 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -860,117 +860,227 @@ describe('fire hooks', () => { state.userCancel(), ]; - preEndStates.forEach((previous: State): void => { - it('should call onDragEnd with the drop result', () => { - const result: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source: { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index, - }, - destination: { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index + 1, - }, - reason: 'DROP', - }; - const current: State = { - phase: 'DROP_COMPLETE', - drop: { - pending: null, - result, - }, - drag: null, - dimension: noDimensions, - }; + preEndStates.forEach((previous: State, index: number): void => { + describe(`end state ${index}`, () => { + it('should call onDragEnd with the drop result', () => { + const result: DropResult = { + draggableId: preset.inHome1.descriptor.id, + type: preset.home.descriptor.type, + source: { + droppableId: preset.inHome1.descriptor.droppableId, + index: preset.inHome1.descriptor.index, + }, + destination: { + droppableId: preset.inHome1.descriptor.droppableId, + index: preset.inHome1.descriptor.index + 1, + }, + reason: 'DROP', + }; + const current: State = { + phase: 'DROP_COMPLETE', + drop: { + pending: null, + result, + }, + drag: null, + dimension: noDimensions, + }; - caller.onStateChange(hooks, previous, current); + caller.onStateChange(hooks, previous, current); - if (!current.drop || !current.drop.result) { - throw new Error('invalid state'); - } + if (!current.drop || !current.drop.result) { + throw new Error('invalid state'); + } - const provided: DropResult = current.drop.result; - expect(hooks.onDragEnd).toHaveBeenCalledWith(provided, { - announce: expect.any(Function), + const provided: DropResult = current.drop.result; + expect(hooks.onDragEnd).toHaveBeenCalledWith(provided, { + announce: expect.any(Function), + }); }); - }); - it('should log an error and not call the callback if there is no drop result', () => { - const invalid: State = { - ...state.dropComplete(), - drop: null, - }; + it('should log an error and not call the callback if there is no drop result', () => { + const invalid: State = { + ...state.dropComplete(), + drop: null, + }; - caller.onStateChange(hooks, previous, invalid); + caller.onStateChange(hooks, previous, invalid); - expect(hooks.onDragEnd).not.toHaveBeenCalled(); - expect(console.error).toHaveBeenCalled(); - }); + expect(hooks.onDragEnd).not.toHaveBeenCalled(); + expect(console.error).toHaveBeenCalled(); + }); - it('should call onDragEnd with null as the destination if there is no destination', () => { - const result: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source: { + it('should call onDragEnd with null as the destination if there is no destination', () => { + const result: DropResult = { + draggableId: preset.inHome1.descriptor.id, + type: preset.home.descriptor.type, + source: { + droppableId: preset.inHome1.descriptor.droppableId, + index: preset.inHome1.descriptor.index, + }, + destination: null, + reason: 'DROP', + }; + const current: State = { + phase: 'DROP_COMPLETE', + drop: { + pending: null, + result, + }, + drag: null, + dimension: noDimensions, + }; + + caller.onStateChange(hooks, previous, current); + + expect(hooks.onDragEnd).toHaveBeenCalledWith(result, { + announce: expect.any(Function), + }); + }); + + it('should call onDragEnd with original source if the item did not move', () => { + const source: DraggableLocation = { droppableId: preset.inHome1.descriptor.droppableId, index: preset.inHome1.descriptor.index, - }, - destination: null, - reason: 'DROP', - }; - const current: State = { - phase: 'DROP_COMPLETE', - drop: { - pending: null, - result, - }, - drag: null, - dimension: noDimensions, - }; + }; + const result: DropResult = { + draggableId: preset.inHome1.descriptor.id, + type: preset.home.descriptor.type, + source, + destination: source, + reason: 'DROP', + }; + const current: State = { + phase: 'DROP_COMPLETE', + drop: { + pending: null, + result, + }, + drag: null, + dimension: noDimensions, + }; + const expected: DropResult = { + draggableId: result.draggableId, + type: result.type, + source: result.source, + // destination has been cleared + destination: source, + reason: 'DROP', + }; - caller.onStateChange(hooks, previous, current); + caller.onStateChange(hooks, previous, current); - expect(hooks.onDragEnd).toHaveBeenCalledWith(result, { - announce: expect.any(Function), + expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, { + announce: expect.any(Function), + }); }); - }); - it('should call onDragEnd with original source if the item did not move', () => { - const source: DraggableLocation = { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index, - }; - const result: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - source, - destination: source, - reason: 'DROP', - }; - const current: State = { - phase: 'DROP_COMPLETE', - drop: { - pending: null, - result, - }, - drag: null, - dimension: noDimensions, - }; - const expected : DropResult = { - draggableId: result.draggableId, - type: result.type, - source: result.source, - // destination has been cleared - destination: source, - reason: 'DROP', - }; + describe('announcements', () => { + const result: DropResult = { + draggableId: preset.inHome1.descriptor.id, + type: preset.home.descriptor.type, + source: { + droppableId: preset.inHome1.descriptor.droppableId, + index: preset.inHome1.descriptor.index, + }, + destination: { + droppableId: preset.inHome1.descriptor.droppableId, + index: preset.inHome1.descriptor.index + 1, + }, + reason: 'DROP', + }; + const current: State = { + phase: 'DROP_COMPLETE', + drop: { + pending: null, + result, + }, + drag: null, + dimension: noDimensions, + }; - caller.onStateChange(hooks, previous, current); + const perform = (myHooks: Hooks) => { + caller.onStateChange(myHooks, previous, current); + }; - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, { - announce: expect.any(Function), + beforeEach(() => { + // clear its state from previous updates + announceMock.mockReset(); + }); + + it('should announce with the default update message if no message is provided', () => { + perform(hooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(result)); + }); + + it('should announce with the default update message if no onDragEnd hook is provided', () => { + const customHooks: Hooks = { + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(result)); + }); + + it('should announce with a provided message', () => { + const customHooks: Hooks = { + onDragEnd: (drop: DropResult, provided: HookProvided) => provided.announce('the end'), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(announceMock).toHaveBeenCalledWith('the end'); + }); + + it('should prevent double announcing', () => { + let myAnnounce: ?Announce; + const customHooks: Hooks = { + onDragEnd: (drop: DropResult, provided: HookProvided) => { + myAnnounce = provided.announce; + myAnnounce('test'); + }, + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith('test'); + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).not.toHaveBeenCalled(); + + if (!myAnnounce) { + throw new Error('Invalid test setup'); + } + + myAnnounce('second'); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); + + it('should prevent async announcing', () => { + const customHooks: Hooks = { + onDragEnd: (drop: DropResult, provided: HookProvided) => { + setTimeout(() => { + // boom + provided.announce('too late'); + }); + }, + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(result)); + expect(console.warn).not.toHaveBeenCalled(); + + // not releasing the async message + jest.runOnlyPendingTimers(); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); }); }); }); @@ -1056,6 +1166,7 @@ describe('fire hooks', () => { caller.onStateChange(hooks, current, current); expect(hooks.onDragStart).not.toHaveBeenCalled(); + expect(hooks.onDragUpdate).not.toHaveBeenCalled(); expect(hooks.onDragEnd).not.toHaveBeenCalled(); }); }); From b0aa6d169cae8aa9eca96edf4a01e46147471bb4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 16:24:01 +1100 Subject: [PATCH 105/163] tests added --- test/unit/state/hook-caller.spec.js | 110 ++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 13 deletions(-) diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index 8ff17492b0..176cb2e7f0 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -1088,22 +1088,21 @@ describe('fire hooks', () => { describe('drag cleared', () => { describe('cleared while dragging', () => { + const drop: DropResult = { + draggableId: preset.inHome1.descriptor.id, + type: preset.home.descriptor.type, + // $ExpectError - not checking for null + source: { + index: preset.inHome1.descriptor.index, + droppableId: preset.inHome1.descriptor.droppableId, + }, + destination: null, + reason: 'CANCEL', + }; it('should return a result with a null destination', () => { - const expected: DropResult = { - draggableId: preset.inHome1.descriptor.id, - type: preset.home.descriptor.type, - // $ExpectError - not checking for null - source: { - index: preset.inHome1.descriptor.index, - droppableId: preset.inHome1.descriptor.droppableId, - }, - destination: null, - reason: 'CANCEL', - }; - caller.onStateChange(hooks, state.dragging(), state.idle); - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, { + expect(hooks.onDragEnd).toHaveBeenCalledWith(drop, { announce: expect.any(Function), }); }); @@ -1121,6 +1120,91 @@ describe('fire hooks', () => { expect(hooks.onDragEnd).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalled(); }); + + describe('announcements', () => { + const perform = (myHooks: Hooks) => { + caller.onStateChange(myHooks, state.dragging(), state.idle); + }; + + beforeEach(() => { + // clear its state from previous updates + announceMock.mockReset(); + }); + + it('should announce with the default update message if no message is provided', () => { + perform(hooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(drop)); + }); + + it('should announce with the default update message if no onDragEnd hook is provided', () => { + const customHooks: Hooks = { + onDragEnd: jest.fn(), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(drop)); + }); + + it('should announce with a provided message', () => { + const customHooks: Hooks = { + onDragEnd: (dropResult: DropResult, provided: HookProvided) => provided.announce('the end'), + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(announceMock).toHaveBeenCalledWith('the end'); + }); + + it('should prevent double announcing', () => { + let myAnnounce: ?Announce; + const customHooks: Hooks = { + onDragEnd: (dropResult: DropResult, provided: HookProvided) => { + myAnnounce = provided.announce; + myAnnounce('test'); + }, + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith('test'); + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).not.toHaveBeenCalled(); + + if (!myAnnounce) { + throw new Error('Invalid test setup'); + } + + myAnnounce('second'); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); + + it('should prevent async announcing', () => { + const customHooks: Hooks = { + onDragEnd: (dropResult: DropResult, provided: HookProvided) => { + setTimeout(() => { + // boom + provided.announce('too late'); + }); + }, + }; + + perform(customHooks); + + expect(announceMock).toHaveBeenCalledWith(messagePreset.onDragEnd(drop)); + expect(console.warn).not.toHaveBeenCalled(); + + // not releasing the async message + jest.runOnlyPendingTimers(); + + expect(announceMock).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalled(); + }); + }); }); // this should never really happen - but just being safe From d55548044ae30d034f60adff8fda5bccf5a04f72 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 16:58:24 +1100 Subject: [PATCH 106/163] streamlining conditional annoucements in hook caller --- src/index.js | 3 + src/state/hooks/hook-caller.js | 162 ++++++++++++++------------------- src/types.js | 11 ++- 3 files changed, 80 insertions(+), 96 deletions(-) diff --git a/src/index.js b/src/index.js index 5eb671d441..a5fa1f98e6 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,9 @@ export type { HookProvided, Announce, DraggableLocation, + OnDragStartHook, + OnDragUpdateHook, + OnDragEndHook, } from './types'; // Droppable diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index aecc0064ff..9578abf5a8 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -13,6 +13,9 @@ import type { DraggableLocation, DraggableDescriptor, DroppableDimension, + OnDragStartHook, + OnDragUpdateHook, + OnDragEndHook, } from '../../types'; type State = { @@ -22,6 +25,8 @@ type State = { hasMovedFromStartLocation: boolean, } +type AnyHookFn = OnDragStartHook | OnDragUpdateHook | OnDragEndHook; + const notDragging: State = { isDragging: false, start: null, @@ -73,13 +78,13 @@ const getAnnouncerForConsumer = (announce: Announce) => { }; // getter for isExpired + // using this technique so that a consumer cannot + // set the isExpired or wasCalled flags result.wasCalled = (): boolean => wasCalled; return result; }; -type OnDragUpdate = (update: DragUpdate, provided: HookProvided) => void; - export default (announce: Announce): HookCaller => { let state: State = notDragging; @@ -117,73 +122,79 @@ export default (announce: Announce): HookCaller => { return start; }; - const onDrag = (() => { - const announceMessage = (update: DragUpdate, onDragUpdate: ?OnDragUpdate) => { - if (!onDragUpdate) { - announce(messagePreset.onDragUpdate(update)); - return; - } - - const provided: HookProvided = { - announce: getAnnouncerForConsumer(announce), - }; - onDragUpdate(update, provided); + const execute = ( + hook: ?AnyHookFn, + data: DragStart | DragUpdate | DropResult, + getDefaultMessage: () => string, + ) => { + // if no hook: announce the default message + if (!hook) { + announce(getDefaultMessage()); + return; + } - // if they do not announce - use the default - if (!provided.announce.wasCalled()) { - announce(messagePreset.onDragUpdate(update)); - } + const managed: Announce = getAnnouncerForConsumer(announce); + const provided: HookProvided = { + announce: managed, }; - return (current: AppState, onDragUpdate?: OnDragUpdate) => { - if (!state.isDragging) { - console.error('Cannot process dragging update if drag has not started'); - return; - } - - const drag: ?DragState = current.drag; - const start: ?DragStart = getDragStart(current); - if (!start || !drag) { - console.error('Cannot update drag when there is invalid state'); - return; - } + hook((data: any), provided); - const destination: ?DraggableLocation = drag.impact.destination; - const update: DragUpdate = { - draggableId: start.draggableId, - type: start.type, - source: start.source, - destination, - }; + if (!managed.wasCalled()) { + announce(getDefaultMessage()); + } + }; - if (!state.hasMovedFromStartLocation) { - // has not moved past the home yet - if (areLocationsEqual(start.source, destination)) { - return; - } + const onDrag = (current: AppState, onDragUpdate: ?OnDragUpdateHook) => { + if (!state.isDragging) { + console.error('Cannot process dragging update if drag has not started'); + return; + } - // We have now moved past the home location - setState({ - lastDestination: destination, - hasMovedFromStartLocation: true, - }); + const drag: ?DragState = current.drag; + const start: ?DragStart = getDragStart(current); + if (!start || !drag) { + console.error('Cannot update drag when there is invalid state'); + return; + } - announceMessage(update, onDragUpdate); - return; - } + const destination: ?DraggableLocation = drag.impact.destination; + const update: DragUpdate = { + draggableId: start.draggableId, + type: start.type, + source: start.source, + destination, + }; - // has not moved from the previous location - if (areLocationsEqual(state.lastDestination, destination)) { + if (!state.hasMovedFromStartLocation) { + // has not moved past the home yet + if (areLocationsEqual(start.source, destination)) { return; } + // We have now moved past the home location setState({ lastDestination: destination, + hasMovedFromStartLocation: true, }); - announceMessage(update, onDragUpdate); - }; - })(); + execute(onDragUpdate, update, () => messagePreset.onDragUpdate(update)); + + // announceMessage(update, onDragUpdate); + return; + } + + // has not moved from the previous location + if (areLocationsEqual(state.lastDestination, destination)) { + return; + } + + setState({ + lastDestination: destination, + }); + + execute(onDragUpdate, update, () => messagePreset.onDragUpdate(update)); + }; const onStateChange = (hooks: Hooks, previous: AppState, current: AppState): void => { const { onDragStart, onDragUpdate, onDragEnd } = hooks; @@ -223,20 +234,7 @@ export default (announce: Announce): HookCaller => { }); // onDragStart is optional - if (!onDragStart) { - announce(messagePreset.onDragStart(start)); - return; - } - - const provided: HookProvided = { - announce: getAnnouncerForConsumer(announce), - }; - onDragStart(start, provided); - - // if they did not announce anything - use the default - if (!provided.announce.wasCalled()) { - announce(messagePreset.onDragStart(start)); - } + execute(onDragStart, start, () => messagePreset.onDragStart(start)); return; } @@ -248,14 +246,7 @@ export default (announce: Announce): HookCaller => { } const result: DropResult = current.drop.result; - const provided: HookProvided = { - announce: getAnnouncerForConsumer(announce), - }; - onDragEnd(result, provided); - - if (!provided.announce.wasCalled()) { - announce(messagePreset.onDragEnd(result)); - } + execute(onDragEnd, result, () => messagePreset.onDragEnd(result)); return; } @@ -286,15 +277,7 @@ export default (announce: Announce): HookCaller => { reason: 'CANCEL', }; - const provided: HookProvided = { - announce: getAnnouncerForConsumer(announce), - }; - onDragEnd(result, provided); - - if (!provided.announce.wasCalled()) { - announce(messagePreset.onDragEnd(result)); - } - + execute(onDragEnd, result, () => messagePreset.onDragEnd(result)); return; } @@ -314,14 +297,7 @@ export default (announce: Announce): HookCaller => { reason: 'CANCEL', }; - const provided: HookProvided = { - announce: getAnnouncerForConsumer(announce), - }; - onDragEnd(result, provided); - - if (!provided.announce.wasCalled()) { - announce(messagePreset.onDragEnd(result)); - } + execute(onDragEnd, result, () => messagePreset.onDragEnd(result)); } }; diff --git a/src/types.js b/src/types.js index 28b2c5dd4d..72a3980c61 100644 --- a/src/types.js +++ b/src/types.js @@ -322,9 +322,14 @@ export type HookProvided = {| announce: Announce, |} +export type OnDragStartHook = (start: DragStart, provided: HookProvided) => void; +export type OnDragUpdateHook = (update: DragUpdate, provided: HookProvided) => void; +export type OnDragEndHook = (result: DropResult, provided: HookProvided) => void; + export type Hooks = {| - onDragStart?: (start: DragStart, provided: HookProvided) => void, - onDragUpdate?: (update: DragUpdate, provided: HookProvided) => void, - onDragEnd: (result: DropResult, provided: HookProvided) => void, + onDragStart?: OnDragStartHook, + onDragUpdate?: OnDragUpdateHook, + // always required + onDragEnd: OnDragEndHook, |} From a5b2bc01ff38a212281a7b372753104a1a40fa4c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 12 Feb 2018 17:21:02 +1100 Subject: [PATCH 107/163] fixing bug where mouse dragging would initially publish incorrect values to onDragUpdate --- src/state/action-creators.js | 32 ++++----- .../dimension-marshal-types.js | 4 +- .../dimension-marshal/dimension-marshal.js | 6 +- src/state/reducer.js | 65 +++++++++++++------ src/types.js | 7 +- src/view/announcer/announcer.js | 1 - .../drag-drop-context/drag-drop-context.jsx | 12 ++-- 7 files changed, 76 insertions(+), 51 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 694daf4f19..426ff2cb60 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -80,26 +80,26 @@ export const completeLift = ( }, }); -export type PublishDraggableDimensionsAction = {| - type: 'PUBLISH_DRAGGABLE_DIMENSIONS', - payload: DraggableDimension[] +export type PublishDraggableDimensionAction = {| + type: 'PUBLISH_DRAGGABLE_DIMENSION', + payload: DraggableDimension, |} -export const publishDraggableDimensions = - (dimensions: DraggableDimension[]): PublishDraggableDimensionsAction => ({ - type: 'PUBLISH_DRAGGABLE_DIMENSIONS', - payload: dimensions, +export const publishDraggableDimension = + (dimension: DraggableDimension): PublishDraggableDimensionAction => ({ + type: 'PUBLISH_DRAGGABLE_DIMENSION', + payload: dimension, }); -export type PublishDroppableDimensionsAction = {| - type: 'PUBLISH_DROPPABLE_DIMENSIONS', - payload: DroppableDimension[] +export type PublishDroppableDimensionAction = {| + type: 'PUBLISH_DROPPABLE_DIMENSION', + payload: DroppableDimension, |} -export const publishDroppableDimensions = - (dimensions: DroppableDimension[]): PublishDroppableDimensionsAction => ({ - type: 'PUBLISH_DROPPABLE_DIMENSIONS', - payload: dimensions, +export const publishDroppableDimension = + (dimension: DroppableDimension): PublishDroppableDimensionAction => ({ + type: 'PUBLISH_DROPPABLE_DIMENSION', + payload: dimension, }); export type BulkPublishDimensionsAction = {| @@ -508,8 +508,8 @@ export const lift = (id: DraggableId, export type Action = CompleteLiftAction | RequestDimensionsAction | - PublishDraggableDimensionsAction | - PublishDroppableDimensionsAction | + PublishDraggableDimensionAction | + PublishDroppableDimensionAction | BulkPublishDimensionsAction | UpdateDroppableDimensionScrollAction | UpdateDroppableDimensionIsEnabledAction | diff --git a/src/state/dimension-marshal/dimension-marshal-types.js b/src/state/dimension-marshal/dimension-marshal-types.js index a6e3cf0af6..68c8237eb9 100644 --- a/src/state/dimension-marshal/dimension-marshal-types.js +++ b/src/state/dimension-marshal/dimension-marshal-types.js @@ -66,8 +66,8 @@ export type DimensionMarshal = {| export type Callbacks = {| cancel: () => void, - publishDraggables: (DraggableDimension[]) => void, - publishDroppables: (DroppableDimension[]) => void, + publishDraggable: (DraggableDimension) => void, + publishDroppable: (DroppableDimension) => void, bulkPublish: ( draggables: DraggableDimension[], droppables: DroppableDimension[], diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index 240c59fa08..7c3b37485f 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -321,8 +321,8 @@ export default (callbacks: Callbacks) => { const home: DroppableDimension = homeEntry.callbacks.getDimension(); const draggable: DraggableDimension = draggableEntry.getDimension(); // Publishing dimensions - callbacks.publishDroppables([home]); - callbacks.publishDraggables([draggable]); + callbacks.publishDroppable(home); + callbacks.publishDraggable(draggable); // Watching the scroll of the home droppable homeEntry.callbacks.watchScroll(); }; @@ -379,8 +379,6 @@ export default (callbacks: Callbacks) => { entry.callbacks.watchScroll(); }); - // callbacks.initialCollectionComplete(); - setFrameId(null); }); diff --git a/src/state/reducer.js b/src/state/reducer.js index 0ef4382563..8a9c4409ce 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -108,6 +108,7 @@ const move = ({ page, shouldAnimate, windowScroll: currentWindowScroll, + hasCompletedFirstBulkPublish: previous.hasCompletedFirstBulkPublish, }; const newImpact: DragImpact = (impact || getDragImpact({ @@ -186,19 +187,14 @@ export default (state: State = clean('IDLE'), action: Action): State => { }; } - if (action.type === 'PUBLISH_DRAGGABLE_DIMENSIONS') { - const dimensions: DraggableDimension[] = action.payload; + if (action.type === 'PUBLISH_DRAGGABLE_DIMENSION') { + const dimension: DraggableDimension = action.payload; if (!canPublishDimension(state.phase)) { console.warn('dimensions rejected as no longer allowing dimension capture in phase', state.phase); return state; } - const additions: DraggableDimensionMap = dimensions.reduce((previous, current) => { - previous[current.descriptor.id] = current; - return previous; - }, {}); - const newState: State = { ...state, dimension: { @@ -206,7 +202,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { droppable: state.dimension.droppable, draggable: { ...state.dimension.draggable, - ...additions, + [dimension.descriptor.id]: dimension, }, }, }; @@ -214,19 +210,14 @@ export default (state: State = clean('IDLE'), action: Action): State => { return updateStateAfterDimensionChange(newState); } - if (action.type === 'PUBLISH_DROPPABLE_DIMENSIONS') { - const dimensions: DroppableDimension[] = action.payload; + if (action.type === 'PUBLISH_DROPPABLE_DIMENSION') { + const dimension: DroppableDimension = action.payload; if (!canPublishDimension(state.phase)) { console.warn('dimensions rejected as no longer allowing dimension capture in phase', state.phase); return state; } - const additions: DroppableDimensionMap = dimensions.reduce((previous, current) => { - previous[current.descriptor.id] = current; - return previous; - }, {}); - const newState: State = { ...state, dimension: { @@ -234,7 +225,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { draggable: state.dimension.draggable, droppable: { ...state.dimension.droppable, - ...additions, + [dimension.descriptor.id]: dimension, }, }, }; @@ -261,8 +252,31 @@ export default (state: State = clean('IDLE'), action: Action): State => { return previous; }, {}); + const drag: ?DragState = (() => { + const existing: ?DragState = state.drag; + if (!existing) { + return null; + } + + if (existing.current.hasCompletedFirstBulkPublish) { + return existing; + } + + // $ExpectError - using spread + const newDrag: DragState = { + ...existing, + current: { + ...existing.current, + hasCompletedFirstBulkPublish: true, + }, + }; + + return newDrag; + })(); + const newState: State = { ...state, + drag, dimension: { request: state.dimension.request, draggable: { @@ -320,6 +334,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { offset: origin, }, windowScroll, + hasCompletedFirstBulkPublish: false, shouldAnimate: false, }; @@ -436,7 +451,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { } if (action.type === 'MOVE') { - // TODO: finished initial collection? // Otherwise get an incorrect index calculated before the other dimensions are published const { client, windowScroll, shouldAnimate } = action.payload; const drag: ?DragState = state.drag; @@ -446,9 +460,20 @@ export default (state: State = clean('IDLE'), action: Action): State => { return state; } - // If we are jump scrolling - manual movements should not update the impact - const impact: ?DragImpact = drag.initial.autoScrollMode === 'JUMP' ? - drag.impact : null; + const impact: ?DragImpact = (() => { + // we do not want to recalculate the initial impact until the first bulk publish is finished + if (!drag.current.hasCompletedFirstBulkPublish) { + console.log('have not completed first bulk publish'); + return drag.impact; + } + + // If we are jump scrolling - manual movements should not update the impact + if (drag.initial.autoScrollMode === 'JUMP') { + return drag.impact; + } + + return null; + })(); return move({ state, diff --git a/src/types.js b/src/types.js index 72a3980c61..e805987943 100644 --- a/src/types.js +++ b/src/types.js @@ -222,8 +222,11 @@ export type CurrentDrag = {| windowScroll: Position, // whether or not draggable movements should be animated shouldAnimate: boolean, - // has the initial dimension capture completed? - // isInitialDimensionCaptureCompleted: boolean, + // We do not want to calculate drag impacts until we have completed + // the first bulk publish. Otherwise the onDragUpdate hook will + // be called with incorrect indexes. + // Before the first bulk publish the calculations will return incorrect indexes. + hasCompletedFirstBulkPublish: boolean, |} // published when a drag starts diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js index 0c36c5927c..6d413b7a09 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/announcer/announcer.js @@ -43,7 +43,6 @@ export default (): Announcer => { } el.textContent = message; - // console.log('announcing:', message); }; const mount = () => { diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index b89dfe837c..e8751e77d0 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -38,8 +38,8 @@ import { import { clean, move, - publishDraggableDimensions, - publishDroppableDimensions, + publishDraggableDimension, + publishDroppableDimension, updateDroppableDimensionScroll, updateDroppableDimensionIsEnabled, bulkPublishDimensions, @@ -112,11 +112,11 @@ export default class DragDropContext extends React.Component { cancel: () => { this.store.dispatch(clean()); }, - publishDraggables: (dimensions: DraggableDimension[]) => { - this.store.dispatch(publishDraggableDimensions(dimensions)); + publishDraggable: (dimension: DraggableDimension) => { + this.store.dispatch(publishDraggableDimension(dimension)); }, - publishDroppables: (dimensions: DroppableDimension[]) => { - this.store.dispatch(publishDroppableDimensions(dimensions)); + publishDroppable: (dimension: DroppableDimension) => { + this.store.dispatch(publishDroppableDimension(dimension)); }, bulkPublish: (draggables: DraggableDimension[], droppables: DroppableDimension[]) => { this.store.dispatch(bulkPublishDimensions(draggables, droppables)); From d2c9e98df11ae7730cd025f0612102ebc7a1e231 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 09:30:35 +1100 Subject: [PATCH 108/163] updating dimension tests --- src/state/dimension.js | 2 +- .../integration/hooks-integration.spec.js | 2 + .../lift-action-and-dimension-marshal.spec.js | 24 +- test/unit/state/dimension.spec.js | 236 +++++++++--------- 4 files changed, 134 insertions(+), 130 deletions(-) diff --git a/src/state/dimension.js b/src/state/dimension.js index a460fab349..db52a07348 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -222,7 +222,7 @@ export const getDroppableDimension = ({ withoutMargin: getArea(withWindowScroll), withMargin: subject, withMarginAndPadding: - getArea(expandBySpacing(withWindowScroll, expandBySpacing(margin, padding))), + getArea(expandBySpacing(expandBySpacing(withWindowScroll, margin), padding)), }, viewport, }; diff --git a/test/unit/integration/hooks-integration.spec.js b/test/unit/integration/hooks-integration.spec.js index eaea69e84f..9e824e57cf 100644 --- a/test/unit/integration/hooks-integration.spec.js +++ b/test/unit/integration/hooks-integration.spec.js @@ -57,6 +57,7 @@ describe('hooks integration', () => { return mount( @@ -90,6 +91,7 @@ describe('hooks integration', () => { jest.useFakeTimers(); hooks = { onDragStart: jest.fn(), + onDragUpdate: jest.fn(), onDragEnd: jest.fn(), }; wrapper = getMountedApp(); diff --git a/test/unit/integration/lift-action-and-dimension-marshal.spec.js b/test/unit/integration/lift-action-and-dimension-marshal.spec.js index 82e3507138..4dbe12b285 100644 --- a/test/unit/integration/lift-action-and-dimension-marshal.spec.js +++ b/test/unit/integration/lift-action-and-dimension-marshal.spec.js @@ -7,8 +7,9 @@ import fireHooks from '../../../src/state/fire-hooks'; import { lift, clean, - publishDraggableDimensions, - publishDroppableDimensions, + publishDraggableDimension, + publishDroppableDimension, + bulkPublishDimensions, updateDroppableDimensionScroll, moveForward, drop, @@ -35,11 +36,14 @@ const getDimensionMarshal = (store: Store): DimensionMarshal => { cancel: () => { store.dispatch(clean()); }, - publishDraggables: (dimensions: DraggableDimension[]) => { - store.dispatch(publishDraggableDimensions(dimensions)); + publishDraggable: (dimension: DraggableDimension) => { + store.dispatch(publishDraggableDimension(dimension)); }, - publishDroppables: (dimensions: DroppableDimension[]) => { - store.dispatch(publishDroppableDimensions(dimensions)); + publishDroppable: (dimension: DroppableDimension) => { + store.dispatch(publishDroppableDimension(dimension)); + }, + bulkPublish: (draggables: DraggableDimension[], droppables: DroppableDimension[]) => { + store.dispatch(bulkPublishDimensions(draggables, droppables)); }, updateDroppableScroll: (id: DroppableId, offset: Position) => { store.dispatch(updateDroppableDimensionScroll(id, offset)); @@ -66,12 +70,14 @@ describe('lifting and the dimension marshal', () => { it('should have the correct indexes in the descriptor post lift', () => { const store: Store = createStore(); const dimensionMarshal: DimensionMarshal = getDimensionMarshal(store); + const caller: HookCaller = createHookCaller(() => { }); // register home dimensions dimensionMarshal.registerDroppable(preset.home.descriptor, { getDimension: () => preset.home, watchScroll: () => { }, unwatchScroll: () => { }, + scroll: () => { }, }); preset.inHomeList.forEach((dimension: DraggableDimension) => { dimensionMarshal.registerDraggable(dimension.descriptor, () => dimension); @@ -124,7 +130,7 @@ describe('lifting and the dimension marshal', () => { return; } - fireHooks(hooks, previousValue, current); + caller(hooks, previousValue, current); dimensionMarshal.onPhaseChange(current); }); @@ -137,7 +143,7 @@ describe('lifting and the dimension marshal', () => { preset.inHome1.descriptor.id, initial, preset.windowScroll, - true + 'JUMP', )(store.dispatch, store.getState); // drag should be started after flushing all timers @@ -188,7 +194,7 @@ describe('lifting and the dimension marshal', () => { preset.inHome3.descriptor.id, forSecondDrag, origin, - true, + 'JUMP', )(store.dispatch, store.getState); // drag should be started after flushing all timers and all state will be published diff --git a/test/unit/state/dimension.spec.js b/test/unit/state/dimension.spec.js index 3fbbdd2c0e..32a1bc2839 100644 --- a/test/unit/state/dimension.spec.js +++ b/test/unit/state/dimension.spec.js @@ -6,9 +6,10 @@ import { clip, } from '../../../src/state/dimension'; import { vertical, horizontal } from '../../../src/state/axis'; -import { offset } from '../../../src/state/spacing'; +import { offsetByPosition } from '../../../src/state/spacing'; import getArea from '../../../src/state/get-area'; import { negate } from '../../../src/state/position'; +import getMaxScroll from '../../../src/state/get-max-scroll'; import type { Area, Spacing, @@ -17,6 +18,7 @@ import type { Position, DraggableDimension, DroppableDimension, + ClosestScrollable, } from '../../../src/types'; const droppableDescriptor: DroppableDescriptor = { @@ -47,26 +49,11 @@ const windowScroll: Position = { }; const origin: Position = { x: 0, y: 0 }; -const addPosition = (area: Area, point: Position): Area => { - const { top, right, bottom, left } = area; - return getArea({ - top: top + point.y, - left: left + point.x, - bottom: bottom + point.y, - right: right + point.x, - }); -}; - -const addSpacing = (area: Area, spacing: Spacing): Area => { - const { top, right, bottom, left } = area; - return getArea({ - // pulling back to increase size - top: top - spacing.top, - left: left - spacing.left, - // pushing forward to increase size - bottom: bottom + spacing.bottom, - right: right + spacing.right, - }); +const getClosestScrollable = (droppable: DroppableDimension): ClosestScrollable => { + if (!droppable.viewport.closestScrollable) { + throw new Error('Cannot get closest scrollable'); + } + return droppable.viewport.closestScrollable; }; describe('dimension', () => { @@ -128,29 +115,12 @@ describe('dimension', () => { }); describe('droppable dimension', () => { - const frameScroll: Position = { - x: 10, - y: 20, - }; - const dimension: DroppableDimension = getDroppableDimension({ descriptor: droppableDescriptor, client, margin, padding, windowScroll, - frameScroll, - }); - - it('should return the initial scroll as the initial and current scroll', () => { - expect(dimension.viewport.frameScroll).toEqual({ - initial: frameScroll, - current: frameScroll, - diff: { - value: origin, - displacement: origin, - }, - }); }); it('should apply the correct axis', () => { @@ -159,14 +129,12 @@ describe('dimension', () => { client, margin, windowScroll, - frameScroll, }); const withVertical: DroppableDimension = getDroppableDimension({ descriptor: droppableDescriptor, client, margin, windowScroll, - frameScroll, direction: 'vertical', }); const withHorizontal: DroppableDimension = getDroppableDimension({ @@ -174,7 +142,6 @@ describe('dimension', () => { client, margin, windowScroll, - frameScroll, direction: 'horizontal', }); @@ -186,7 +153,7 @@ describe('dimension', () => { expect(withHorizontal.axis).toBe(horizontal); }); - describe('without scroll (client)', () => { + describe('without window scroll (client)', () => { it('should return a portion that does not consider margins', () => { const area: Area = getArea({ top: client.top, @@ -221,7 +188,7 @@ describe('dimension', () => { }); }); - describe('with scroll (page)', () => { + describe('with window scroll (page)', () => { it('should return a portion that does not consider margins', () => { const area: Area = getArea({ top: client.top + windowScroll.y, @@ -248,79 +215,114 @@ describe('dimension', () => { const area: Area = getArea({ top: (client.top + windowScroll.y) - margin.top - padding.top, left: (client.left + windowScroll.x) - margin.left - padding.left, - bottom: client.bottom + windowScroll.y + margin.bottom + padding.bottom, - right: client.right + windowScroll.x + margin.right + padding.right, + bottom: (client.bottom + windowScroll.y) + margin.bottom + padding.bottom, + right: (client.right + windowScroll.x) + margin.right + padding.right, }); expect(dimension.page.withMarginAndPadding).toEqual(area); }); }); - describe('viewport', () => { - it('should use the area as the frame if no frame is provided', () => { - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: droppableDescriptor, - client, - margin, - windowScroll: origin, - frameScroll, + describe('closest scrollable', () => { + describe('basic info about the scrollable', () => { + const client: Area = getArea({ + top: 0, + right: 300, + bottom: 300, + left: 0, + }); + const frameClient: Area = getArea({ + top: 0, + right: 100, + bottom: 100, + left: 0, }); - expect(droppable.viewport.frame).toEqual(addSpacing(client, margin)); - }); - - it('should include the window scroll', () => { - const droppable: DroppableDimension = getDroppableDimension({ + const withScrollable: DroppableDimension = getDroppableDimension({ descriptor: droppableDescriptor, client, - margin, windowScroll, - frameScroll, + closest: { + frameClient, + scrollWidth: 500, + scrollHeight: 500, + scroll: { x: 10, y: 10 }, + shouldClipSubject: true, + }, }); - expect(droppable.viewport.frame).toEqual( - addPosition(addSpacing(client, margin), windowScroll), - ); - }); + it('should not have a closest scrollable if there is no closest scrollable', () => { + const noClosestScrollable: DroppableDimension = getDroppableDimension({ + descriptor: droppableDescriptor, + client, + }); - it('should use the frameClient as the frame if provided', () => { - const frameClient: Area = getArea({ - top: 20, - left: 30, - right: 40, - bottom: 50, + expect(noClosestScrollable.viewport.closestScrollable).toBe(null); + expect(noClosestScrollable.viewport.subject) + .toEqual(noClosestScrollable.viewport.clipped); }); - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: droppableDescriptor, - client, - frameClient, + it('should offset the frame client by the window scroll', () => { + expect(getClosestScrollable(withScrollable).frame).toEqual( + getArea(offsetByPosition(frameClient, windowScroll)) + ); }); - expect(droppable.viewport.frame).toEqual(frameClient); + it('should set the max scroll point for the closest scrollable', () => { + expect(getClosestScrollable(withScrollable).scroll.max).toEqual({ x: 400, y: 400 }); + }); }); describe('frame clipping', () => { + const frameClient = getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }); + const getWithClient = (subject: Area): DroppableDimension => getDroppableDimension({ + descriptor: droppableDescriptor, + client: subject, + closest: { + frameClient, + scrollWidth: 300, + scrollHeight: 300, + scroll: origin, + shouldClipSubject: true, + }, + }); + + it('should not clip the frame if requested not to', () => { + const withoutClipping: DroppableDimension = getDroppableDimension({ + descriptor: droppableDescriptor, + client, + windowScroll, + closest: { + frameClient, + scrollWidth: 300, + scrollHeight: 300, + scroll: origin, + // disabling clipping + shouldClipSubject: false, + }, + }); + + expect(withoutClipping.viewport.subject).toEqual(withoutClipping.viewport.clipped); + + expect(getClosestScrollable(withoutClipping).shouldClipSubject).toBe(false); + }); + describe('frame is smaller than subject', () => { it('should clip the subject to the size of the frame', () => { const subject = getArea({ top: 0, - right: 100, - bottom: 100, left: 0, - }); - const frameClient = getArea({ - top: 10, - right: 90, - bottom: 90, - left: 10, + // 100px bigger than the frame on the bottom and right + bottom: 200, + right: 200, }); - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: droppableDescriptor, - client: subject, - frameClient, - }); + const droppable: DroppableDimension = getWithClient(subject); expect(droppable.viewport.clipped).toEqual(frameClient); }); @@ -328,12 +330,7 @@ describe('dimension', () => { describe('frame is larger than subject', () => { it('should return a clipped size that is equal to that of the subject', () => { - const frameClient = getArea({ - top: 0, - right: 100, - bottom: 100, - left: 0, - }); + // 10px smaller on every side const subject = getArea({ top: 10, right: 90, @@ -341,24 +338,13 @@ describe('dimension', () => { left: 10, }); - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: droppableDescriptor, - client: subject, - frameClient, - }); + const droppable: DroppableDimension = getWithClient(subject); expect(droppable.viewport.clipped).toEqual(subject); }); }); describe('subject clipped on one side by frame', () => { - const frameClient = getArea({ - top: 0, - right: 100, - bottom: 100, - left: 0, - }); - it('should clip on all sides', () => { // each of these subjects bleeds out past the frame in one direction const subjects: Area[] = [ @@ -381,11 +367,7 @@ describe('dimension', () => { ]; subjects.forEach((subject: Area) => { - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: droppableDescriptor, - client: subject, - frameClient, - }); + const droppable: DroppableDimension = getWithClient(subject); expect(droppable.viewport.clipped).toEqual(frameClient); }); @@ -416,30 +398,44 @@ describe('dimension', () => { const droppable: DroppableDimension = getDroppableDimension({ descriptor: droppableDescriptor, client: subject, - frameClient, - frameScroll, + closest: { + frameClient, + scroll: frameScroll, + scrollWidth: 500, + scrollHeight: 100, + shouldClipSubject: true, + }, }); + const closestScrollable: ClosestScrollable = getClosestScrollable(droppable); + // original frame - expect(droppable.viewport.frame).toEqual(frameClient); + expect(closestScrollable.frame).toEqual(frameClient); // subject is currently clipped by the frame expect(droppable.viewport.clipped).toEqual(frameClient); // scrolling down const newScroll: Position = { x: 0, y: 100 }; const updated: DroppableDimension = scrollDroppable(droppable, newScroll); + const updatedClosest: ClosestScrollable = getClosestScrollable(updated); // unchanged frame client - expect(updated.viewport.frame).toEqual(frameClient); + expect(updatedClosest.frame).toEqual(frameClient); // updated scroll info - expect(updated.viewport.frameScroll).toEqual({ + expect(updatedClosest.scroll).toEqual({ initial: frameScroll, current: newScroll, diff: { value: newScroll, displacement: negate(newScroll), }, + max: getMaxScroll({ + scrollHeight: 100, + scrollWidth: 500, + width: frameClient.width, + height: frameClient.height, + }), }); // updated clipped @@ -481,13 +477,13 @@ describe('dimension', () => { }); const outside: Spacing[] = [ // top - offset(frame, { x: 0, y: -200 }), + offsetByPosition(frame, { x: 0, y: -200 }), // right - offset(frame, { x: 200, y: 0 }), + offsetByPosition(frame, { x: 200, y: 0 }), // bottom - offset(frame, { x: 0, y: 200 }), + offsetByPosition(frame, { x: 0, y: 200 }), // left - offset(frame, { x: -200, y: 0 }), + offsetByPosition(frame, { x: -200, y: 0 }), ]; outside.forEach((subject: Spacing) => { From 8034a9290037eacaed85da14afcad6879982ae45 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 09:38:40 +1100 Subject: [PATCH 109/163] simplying hook-caller --- src/state/hooks/hook-caller.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/state/hooks/hook-caller.js b/src/state/hooks/hook-caller.js index 9578abf5a8..5ff211cca5 100644 --- a/src/state/hooks/hook-caller.js +++ b/src/state/hooks/hook-caller.js @@ -26,6 +26,7 @@ type State = { } type AnyHookFn = OnDragStartHook | OnDragUpdateHook | OnDragEndHook; +type AnyHookData = DragStart | DragUpdate | DropResult; const notDragging: State = { isDragging: false, @@ -124,12 +125,12 @@ export default (announce: Announce): HookCaller => { const execute = ( hook: ?AnyHookFn, - data: DragStart | DragUpdate | DropResult, - getDefaultMessage: () => string, + data: AnyHookData, + getDefaultMessage: (data: any) => string, ) => { // if no hook: announce the default message if (!hook) { - announce(getDefaultMessage()); + announce(getDefaultMessage(data)); return; } @@ -141,7 +142,7 @@ export default (announce: Announce): HookCaller => { hook((data: any), provided); if (!managed.wasCalled()) { - announce(getDefaultMessage()); + announce(getDefaultMessage(data)); } }; @@ -178,7 +179,7 @@ export default (announce: Announce): HookCaller => { hasMovedFromStartLocation: true, }); - execute(onDragUpdate, update, () => messagePreset.onDragUpdate(update)); + execute(onDragUpdate, update, messagePreset.onDragUpdate); // announceMessage(update, onDragUpdate); return; @@ -193,7 +194,7 @@ export default (announce: Announce): HookCaller => { lastDestination: destination, }); - execute(onDragUpdate, update, () => messagePreset.onDragUpdate(update)); + execute(onDragUpdate, update, messagePreset.onDragUpdate); }; const onStateChange = (hooks: Hooks, previous: AppState, current: AppState): void => { @@ -234,7 +235,7 @@ export default (announce: Announce): HookCaller => { }); // onDragStart is optional - execute(onDragStart, start, () => messagePreset.onDragStart(start)); + execute(onDragStart, start, messagePreset.onDragStart); return; } @@ -246,7 +247,7 @@ export default (announce: Announce): HookCaller => { } const result: DropResult = current.drop.result; - execute(onDragEnd, result, () => messagePreset.onDragEnd(result)); + execute(onDragEnd, result, messagePreset.onDragEnd); return; } @@ -277,7 +278,7 @@ export default (announce: Announce): HookCaller => { reason: 'CANCEL', }; - execute(onDragEnd, result, () => messagePreset.onDragEnd(result)); + execute(onDragEnd, result, messagePreset.onDragEnd); return; } @@ -297,7 +298,7 @@ export default (announce: Announce): HookCaller => { reason: 'CANCEL', }; - execute(onDragEnd, result, () => messagePreset.onDragEnd(result)); + execute(onDragEnd, result, messagePreset.onDragEnd); } }; From 437ea6e98c3d651753c0d1e37f68b4e09b0bac00 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 09:58:26 +1100 Subject: [PATCH 110/163] updating tests for spacing --- src/state/spacing.js | 18 ++++++------ test/unit/state/spacing.spec.js | 52 +++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/state/spacing.js b/src/state/spacing.js index 76dd658fd4..62963f3150 100644 --- a/src/state/spacing.js +++ b/src/state/spacing.js @@ -11,15 +11,6 @@ export const offsetByPosition = (spacing: Spacing, point: Position): Spacing => right: spacing.right + point.x, }); -export const expandBySpacing = (spacing1: Spacing, spacing2: Spacing): Spacing => ({ - // pulling back to increase size - top: spacing1.top - spacing2.top, - left: spacing1.left - spacing2.left, - // pushing forward to increase size - bottom: spacing1.bottom + spacing2.bottom, - right: spacing1.right + spacing2.right, -}); - export const expandByPosition = (spacing: Spacing, position: Position): Spacing => ({ // pulling back to increase size top: spacing.top - position.y, @@ -29,6 +20,15 @@ export const expandByPosition = (spacing: Spacing, position: Position): Spacing bottom: spacing.bottom + position.y, }); +export const expandBySpacing = (spacing1: Spacing, spacing2: Spacing): Spacing => ({ + // pulling back to increase size + top: spacing1.top - spacing2.top, + left: spacing1.left - spacing2.left, + // pushing forward to increase size + bottom: spacing1.bottom + spacing2.bottom, + right: spacing1.right + spacing2.right, +}); + export const isEqual = (spacing1: Spacing, spacing2: Spacing): boolean => ( spacing1.top === spacing2.top && spacing1.right === spacing2.right && diff --git a/test/unit/state/spacing.spec.js b/test/unit/state/spacing.spec.js index 5855660ab0..c19103a92d 100644 --- a/test/unit/state/spacing.spec.js +++ b/test/unit/state/spacing.spec.js @@ -1,10 +1,10 @@ // @flow import { - add, - addPosition, isEqual, - offset, getCorners, + expandByPosition, + offsetByPosition, + expandBySpacing, } from '../../../src/state/spacing'; import type { Position, Spacing } from '../../../src/types'; @@ -23,21 +23,9 @@ const spacing2: Spacing = { }; describe('spacing', () => { - describe('add', () => { - it('should add two spacing boxes together', () => { - const expected: Spacing = { - top: 11, - right: 26, - bottom: 37, - left: 14, - }; - expect(add(spacing1, spacing2)).toEqual(expected); - }); - }); - - describe('addPosition', () => { - it('should add a position to the right and bottom bounds of a spacing box', () => { - const spacing = { + describe('expandByPosition', () => { + it('should increase the size of the spacing', () => { + const spacing: Spacing = { top: 0, right: 10, bottom: 10, @@ -48,12 +36,32 @@ describe('spacing', () => { y: 5, }; const expected = { - top: 0, + top: -5, right: 15, bottom: 15, + left: -5, + }; + + expect(expandByPosition(spacing, position)).toEqual(expected); + }); + }); + + describe('expandBySpacing', () => { + it('should increase the size of a spacing by the size of another', () => { + const spacing: Spacing = { + top: 10, + right: 20, + bottom: 20, + left: 10, + }; + const expected: Spacing = { + top: 0, + right: 40, + bottom: 40, left: 0, }; - expect(addPosition(spacing, position)).toEqual(expected); + + expect(expandBySpacing(spacing, spacing)).toEqual(expected); }); }); @@ -72,7 +80,7 @@ describe('spacing', () => { }); }); - describe('offset', () => { + describe('offsetByPosition', () => { it('should add x/y values to top/right/bottom/left dimensions', () => { const offsetPosition: Position = { x: 10, @@ -84,7 +92,7 @@ describe('spacing', () => { bottom: 28, left: 15, }; - expect(offset(spacing1, offsetPosition)).toEqual(expected); + expect(offsetByPosition(spacing1, offsetPosition)).toEqual(expected); }); }); From cf94e0ced4f1b57a91d3491657dec36ba6272510 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 12:15:45 +1100 Subject: [PATCH 111/163] updating get-drag-impact tests --- src/state/action-creators.js | 9 ++- src/state/dimension.js | 2 +- src/state/get-drag-impact/in-foreign-list.js | 11 +-- src/state/get-drag-impact/in-home-list.js | 11 +-- .../move-cross-axis/get-closest-draggable.js | 14 ++-- src/state/visibility/is-visible.js | 6 +- test/setup.js | 7 ++ test/unit/state/get-drag-impact.spec.js | 77 +++++++++++++------ test/utils/dimension.js | 59 +++++++++++--- 9 files changed, 133 insertions(+), 63 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 426ff2cb60..c5acaa40d1 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -17,6 +17,7 @@ import type { AutoScrollMode, } from '../types'; import noImpact from './no-impact'; +import withDroppableDisplacement from './with-droppable-displacement'; import getNewHomeClientCenter from './get-new-home-client-center'; import { add, subtract, isEqual } from './position'; @@ -38,11 +39,11 @@ const getScrollDiff = ({ current.windowScroll ); - const droppableScrollDiff: Position = droppable && droppable.viewport.closestScrollable ? - droppable.viewport.closestScrollable.scroll.diff.displacement : - origin; + if(!droppable) { + return windowScrollDiff; + } - return add(windowScrollDiff, droppableScrollDiff); + return withDroppableDisplacement(droppable, windowScrollDiff); }; export type RequestDimensionsAction = {| diff --git a/src/state/dimension.js b/src/state/dimension.js index db52a07348..8ce95f47d6 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -1,7 +1,7 @@ // @flow import { vertical, horizontal } from './axis'; import getArea from './get-area'; -import { offsetByPosition, expandBySpacing, expandByPosition } from './spacing'; +import { offsetByPosition, expandBySpacing } from './spacing'; import { subtract, negate } from './position'; import getMaxScroll from './get-max-scroll'; import type { diff --git a/src/state/get-drag-impact/in-foreign-list.js b/src/state/get-drag-impact/in-foreign-list.js index 7144a5d7df..30b58cd592 100644 --- a/src/state/get-drag-impact/in-foreign-list.js +++ b/src/state/get-drag-impact/in-foreign-list.js @@ -9,9 +9,10 @@ import type { Displacement, Area, } from '../../types'; -import { add, patch } from '../position'; +import { patch } from '../position'; import getDisplacement from '../get-displacement'; import getViewport from '../../window/get-viewport'; +import withDroppableScroll from '../with-droppable-scroll'; type Args = {| pageCenter: Position, @@ -21,8 +22,6 @@ type Args = {| previousImpact: DragImpact, |} -const origin: Position = { x: 0, y: 0 }; - export default ({ pageCenter, draggable, @@ -38,11 +37,7 @@ export default ({ // To do this we need to consider any displacement caused by // a change in scroll in the droppable we are currently over. - const destinationScrollDiff: Position = destination.viewport.closestScrollable ? - destination.viewport.closestScrollable.scroll.diff.value : - origin; - - const currentCenter: Position = add(pageCenter, destinationScrollDiff); + const currentCenter: Position = withDroppableScroll(destination, pageCenter); const displaced: Displacement[] = insideDestination .filter((child: DraggableDimension): boolean => { diff --git a/src/state/get-drag-impact/in-home-list.js b/src/state/get-drag-impact/in-home-list.js index 8cd6b0c290..934693be32 100644 --- a/src/state/get-drag-impact/in-home-list.js +++ b/src/state/get-drag-impact/in-home-list.js @@ -9,8 +9,9 @@ import type { Displacement, Area, } from '../../types'; -import { add, patch } from '../position'; +import { patch } from '../position'; import getDisplacement from '../get-displacement'; +import withDroppableScroll from '../with-droppable-scroll'; import getViewport from '../../window/get-viewport'; // It is the responsibility of this function @@ -38,15 +39,9 @@ export default ({ // The starting center position const originalCenter: Position = draggable.page.withoutMargin.center; - // Where is the element now? - // Need to take into account the change of scroll in the droppable - const homeScrollDiff: Position = home.viewport.closestScrollable ? - home.viewport.closestScrollable.scroll.diff.value : - origin; - // Where the element actually is now - const currentCenter: Position = add(pageCenter, homeScrollDiff); + const currentCenter: Position = withDroppableScroll(home, pageCenter); // not considering margin so that items move based on visible edges const isBeyondStartPosition: boolean = currentCenter[axis.line] - originalCenter[axis.line] > 0; diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index a078faf81f..6ea0f76415 100644 --- a/src/state/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-cross-axis/get-closest-draggable.js @@ -2,6 +2,7 @@ import { add, distance } from '../position'; import getViewport from '../../window/get-viewport'; import { isTotallyVisible } from '../visibility/is-visible'; +import withDroppableDisplacement from '../with-droppable-displacement'; import type { Area, Axis, @@ -33,9 +34,6 @@ export default ({ } const viewport: Area = getViewport(); - const scrollDisplacement: Position = destination.viewport.closestScrollable ? - destination.viewport.closestScrollable.scroll.diff.displacement : - origin; const result: DraggableDimension[] = insideDestination // Remove any options that are hidden by overflow @@ -48,8 +46,14 @@ export default ({ })) .sort((a: DraggableDimension, b: DraggableDimension): number => { // Need to consider the change in scroll in the destination - const distanceToA = distance(pageCenter, add(a.page.withMargin.center, scrollDisplacement)); - const distanceToB = distance(pageCenter, add(b.page.withMargin.center, scrollDisplacement)); + const distanceToA = distance( + pageCenter, + withDroppableDisplacement(destination, a.page.withMargin.center) + ); + const distanceToB = distance( + pageCenter, + withDroppableDisplacement(destination, b.page.withMargin.center) + ); // if a is closer - return a if (distanceToA < distanceToB) { diff --git a/src/state/visibility/is-visible.js b/src/state/visibility/is-visible.js index 0bb8b6d782..0e20843a47 100644 --- a/src/state/visibility/is-visible.js +++ b/src/state/visibility/is-visible.js @@ -31,7 +31,7 @@ const isVisible = ({ const displacement: Position = destination.viewport.closestScrollable ? destination.viewport.closestScrollable.scroll.diff.displacement : origin; - const withScroll: Spacing = offsetByPosition(target, displacement); + const withDisplacement: Spacing = offsetByPosition(target, displacement); // destination subject is totally hidden by frame // this should never happen - but just guarding against it @@ -44,12 +44,12 @@ const isVisible = ({ // adjust for the scroll as the clipped viewport takes into account // the scroll of the droppable. const isVisibleInDroppable: boolean = - isVisibleThroughFrameFn(destination.viewport.clipped)(withScroll); + isVisibleThroughFrameFn(destination.viewport.clipped)(withDisplacement); // We also need to consider whether the destination scroll when detecting // if we are visible in the viewport. const isVisibleInViewport: boolean = - isVisibleThroughFrameFn(viewport)(withScroll); + isVisibleThroughFrameFn(viewport)(withDisplacement); return isVisibleInDroppable && isVisibleInViewport; }; diff --git a/test/setup.js b/test/setup.js index fe17a64af6..14389de18e 100644 --- a/test/setup.js +++ b/test/setup.js @@ -17,6 +17,13 @@ if (typeof window !== 'undefined') { }); } +// Setting initial viewport +// Need to set clientWidth and clientHeight as jsdom does not set these properties +if (typeof document !== 'undefined' && typeof window !== 'undefined') { + document.documentElement.clientWidth = window.innerWidth; + document.documentElement.clientHeight = window.innerHeight; +} + // setting up global enzyme const Enzyme = require('enzyme'); diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index a353c77cd0..bd704ff3cf 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -13,6 +13,7 @@ import { import { getPreset, disableDroppable, + makeScrollable, } from '../../utils/dimension'; import getViewport from '../../../src/window/get-viewport'; import type { @@ -241,6 +242,12 @@ describe('get drag impact', () => { }); describe('home droppable scroll has changed during a drag', () => { + const scrollableHome: DroppableDimension = makeScrollable(home); + const withScrollableHome = { + ...droppables, + [home.descriptor.id]: scrollableHome, + }; + // moving inHome1 past inHome2 by scrolling the dimension describe('moving beyond start position with own scroll', () => { it('should move past other draggables', () => { @@ -255,12 +262,12 @@ describe('get drag impact', () => { // need to move over the edge patch(axis.line, 1), ); - const homeWithScroll: DroppableDimension = scrollDroppable( - home, distanceNeeded + const scrolledHome: DroppableDimension = scrollDroppable( + scrollableHome, distanceNeeded ); const updatedDroppables: DroppableDimensionMap = { - ...droppables, - [home.descriptor.id]: homeWithScroll, + ...withScrollableHome, + [home.descriptor.id]: scrolledHome, }; // no changes in current page center from original const pageCenter: Position = inHome1.page.withoutMargin.center; @@ -309,12 +316,12 @@ describe('get drag impact', () => { // need to move over the edge patch(axis.line, -1), ); - const homeWithScroll: DroppableDimension = scrollDroppable( - home, distanceNeeded + const scrolledHome: DroppableDimension = scrollDroppable( + scrollableHome, distanceNeeded ); const updatedDroppables: DroppableDimensionMap = { - ...droppables, - [home.descriptor.id]: homeWithScroll, + ...withScrollableHome, + [home.descriptor.id]: scrolledHome, }; // no changes in current page center from original const pageCenter: Position = inHome4.page.withoutMargin.center; @@ -372,13 +379,19 @@ describe('get drag impact', () => { // will be cut by the frame [axis.end]: 200, }), - frameClient: getArea({ - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - // will cut the subject, - [axis.end]: 100, - }), + closest: { + frameClient: getArea({ + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + // will cut the subject, + [axis.end]: 100, + }), + scrollWidth: 100, + scrollHeight: 100, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const visible: DraggableDimension = getDraggableDimension({ descriptor: { @@ -783,9 +796,10 @@ describe('get drag impact', () => { it('should have no impact impact the destination (actual)', () => { // will go over the threshold of inForeign2 so that it will not be displaced forward const scroll: Position = patch(axis.line, 1000); + const scrollableHome: DroppableDimension = makeScrollable(home, 1000); const map: DroppableDimensionMap = { ...droppables, - [home.descriptor.id]: scrollDroppable(home, scroll), + [home.descriptor.id]: scrollDroppable(scrollableHome, scroll), }; const expected: DragImpact = { @@ -870,6 +884,12 @@ describe('get drag impact', () => { }); describe('destination droppable scroll is updated during a drag', () => { + const scrollableForeign: DroppableDimension = makeScrollable(foreign); + const withScrollableForeign = { + ...droppables, + [foreign.descriptor.id]: scrollableForeign, + }; + const pageCenter: Position = patch( axis.line, inForeign2.page.withoutMargin[axis.end] - 1, @@ -881,8 +901,8 @@ describe('get drag impact', () => { // be displaced forward const scroll: Position = patch(axis.line, 1); const map: DroppableDimensionMap = { - ...droppables, - [foreign.descriptor.id]: scrollDroppable(foreign, scroll), + ...withScrollableForeign, + [foreign.descriptor.id]: scrollDroppable(scrollableForeign, scroll), }; const expected: DragImpact = { @@ -994,6 +1014,7 @@ describe('get drag impact', () => { const foreignCrossAxisStart: number = 120; const foreignCrossAxisEnd: number = 200; + const destination: DroppableDimension = getDroppableDimension({ descriptor: { id: 'destination', @@ -1007,13 +1028,19 @@ describe('get drag impact', () => { // will be cut off by the frame [axis.end]: 200, }), - frameClient: getArea({ - [axis.crossAxisStart]: foreignCrossAxisStart, - [axis.crossAxisEnd]: foreignCrossAxisEnd, - [axis.start]: 0, - // will cut off the subject - [axis.end]: 100, - }), + closest: { + frameClient: getArea({ + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.start]: 0, + // will cut off the subject + [axis.end]: 100, + }), + scrollWidth: 100, + scrollHeight: 100, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const visible: DraggableDimension = getDraggableDimension({ descriptor: { diff --git a/test/utils/dimension.js b/test/utils/dimension.js index d607edc8db..78ffe79ca8 100644 --- a/test/utils/dimension.js +++ b/test/utils/dimension.js @@ -1,8 +1,11 @@ // @flow import getArea from '../../src/state/get-area'; +import { patch } from '../../src/state/position'; +import { expandByPosition } from '../../src/state/spacing'; import { getDroppableDimension, getDraggableDimension } from '../../src/state/dimension'; import { vertical } from '../../src/state/axis'; import type { + Area, Axis, Position, Spacing, @@ -12,16 +15,54 @@ import type { DroppableDimensionMap, } from '../../src/types'; +const margin: Spacing = { top: 10, left: 10, bottom: 5, right: 5 }; +const padding: Spacing = { top: 2, left: 2, bottom: 2, right: 2 }; +const windowScroll: Position = { x: 50, y: 100 }; +const crossAxisStart: number = 0; +const crossAxisEnd: number = 100; +const foreignCrossAxisStart: number = 100; +const foreignCrossAxisEnd: number = 200; +const emptyForeignCrossAxisStart: number = 200; +const emptyForeignCrossAxisEnd: number = 300; + +export const makeScrollable = (droppable: DroppableDimension, amount?: number = 20) => { + const axis: Axis = droppable.axis; + const client: Area = droppable.client.withoutMargin; + // is 10px smaller than the client on the main axis + // this will leave 10px of scrollable area. + // only expanding on one axis + const frameClient: Area = getArea({ + top: client.top, + left: client.left, + right: axis === vertical ? client.right : client.right - amount, + bottom: axis === vertical ? client.bottom - amount : client.bottom, + }); + + // add scroll space on the main axis + const scrollSize = { + width: axis === vertical ? client.width : client.width + amount, + height: axis === vertical ? client.height + amount : client.height, + }; + + return getDroppableDimension({ + descriptor: droppable.descriptor, + direction: axis.direction, + padding, + margin, + windowScroll, + client, + closest: { + frameClient, + scrollWidth: scrollSize.width, + scrollHeight: scrollSize.height, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); +} + export const getPreset = (axis?: Axis = vertical) => { - const margin: Spacing = { top: 10, left: 10, bottom: 5, right: 5 }; - const padding: Spacing = { top: 2, left: 2, bottom: 2, right: 2 }; - const windowScroll: Position = { x: 50, y: 100 }; - const crossAxisStart: number = 0; - const crossAxisEnd: number = 100; - const foreignCrossAxisStart: number = 100; - const foreignCrossAxisEnd: number = 200; - const emptyForeignCrossAxisStart: number = 200; - const emptyForeignCrossAxisEnd: number = 300; + const home: DroppableDimension = getDroppableDimension({ descriptor: { From 93333c95d200c769ef8179e65d91b69c619e0968 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 12:49:15 +1100 Subject: [PATCH 112/163] updating dimension marshal tests --- src/state/get-drag-impact/in-home-list.js | 2 - test/unit/view/dimension-marshal.spec.js | 145 ++++++++++++---------- test/utils/dimension.js | 2 - 3 files changed, 79 insertions(+), 70 deletions(-) diff --git a/src/state/get-drag-impact/in-home-list.js b/src/state/get-drag-impact/in-home-list.js index 934693be32..e90d52e061 100644 --- a/src/state/get-drag-impact/in-home-list.js +++ b/src/state/get-drag-impact/in-home-list.js @@ -25,8 +25,6 @@ type Args = {| previousImpact: DragImpact, |} -const origin: Position = { x: 0, y: 0 }; - export default ({ pageCenter, draggable, diff --git a/test/unit/view/dimension-marshal.spec.js b/test/unit/view/dimension-marshal.spec.js index 1a2c5a5833..08c8458461 100644 --- a/test/unit/view/dimension-marshal.spec.js +++ b/test/unit/view/dimension-marshal.spec.js @@ -26,8 +26,9 @@ import type { const getCallbackStub = (): Callbacks => { const callbacks: Callbacks = { cancel: jest.fn(), - publishDraggables: jest.fn(), - publishDroppables: jest.fn(), + publishDraggable: jest.fn(), + publishDroppable: jest.fn(), + bulkPublish: jest.fn(), updateDroppableScroll: jest.fn(), updateDroppableIsEnabled: jest.fn(), }; @@ -86,6 +87,7 @@ const populateMarshal = ( unwatchScroll: () => { watches.droppable.unwatchScroll(id); }, + scroll: () => {}, }; marshal.registerDroppable(droppable.descriptor, callbacks); @@ -192,10 +194,11 @@ describe('dimension marshal', () => { marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1); - expect(callbacks.publishDraggables).toBeCalledWith([preset.inHome1]); - expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1); - expect(callbacks.publishDroppables).toBeCalledWith([preset.home]); + expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1); + expect(callbacks.publishDraggable).toBeCalledWith(preset.inHome1); + expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1); + expect(callbacks.publishDroppable).toBeCalledWith(preset.home); + expect(callbacks.bulkPublish).not.toHaveBeenCalled(); }); it('should ask the home droppable to start listening to scrolling', () => { @@ -219,8 +222,10 @@ describe('dimension marshal', () => { populateMarshal(marshal); marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1); - expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1); + expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1); + expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1); + callbacks.publishDraggable.mockReset(); + callbacks.publishDroppable.mockReset(); // moving to idle state before moving to dragging state marshal.onPhaseChange(state.idle); @@ -229,8 +234,9 @@ describe('dimension marshal', () => { requestAnimationFrame.flush(); // nothing happened - expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1); - expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1); + expect(callbacks.bulkPublish).not.toHaveBeenCalled(); + expect(callbacks.publishDroppable).not.toHaveBeenCalled(); + expect(callbacks.publishDraggable).not.toHaveBeenCalled(); }); }); @@ -241,10 +247,11 @@ describe('dimension marshal', () => { const watchers = populateMarshal(marshal); marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1); - expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1); - callbacks.publishDroppables.mockClear(); - callbacks.publishDraggables.mockClear(); + expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1); + expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1); + expect(callbacks.bulkPublish).not.toHaveBeenCalled(); + callbacks.publishDroppable.mockClear(); + callbacks.publishDraggable.mockClear(); watchers.draggable.getDimension.mockClear(); watchers.droppable.getDimension.mockClear(); @@ -255,7 +262,7 @@ describe('dimension marshal', () => { // flush all timers - would normally collect and publish requestAnimationFrame.flush(); - expect(callbacks.publishDraggables).not.toHaveBeenCalled(); + expect(callbacks.publishDraggable).not.toHaveBeenCalled(); expect(watchers.droppable.getDimension).not.toHaveBeenCalled(); }); @@ -397,8 +404,8 @@ describe('dimension marshal', () => { marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); // clearing initial calls - callbacks.publishDraggables.mockClear(); - callbacks.publishDroppables.mockClear(); + callbacks.publishDraggable.mockClear(); + callbacks.publishDroppable.mockClear(); // execute collection frame marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); @@ -409,8 +416,9 @@ describe('dimension marshal', () => { requestAnimationFrame.step(); // nothing additional called - expect(callbacks.publishDraggables).not.toHaveBeenCalled(); - expect(callbacks.publishDroppables).not.toHaveBeenCalled(); + expect(callbacks.publishDraggable).not.toHaveBeenCalled(); + expect(callbacks.publishDroppable).not.toHaveBeenCalled(); + expect(callbacks.bulkPublish).not.toHaveBeenCalled(); }); it('should publish all the collected draggables', () => { @@ -419,17 +427,17 @@ describe('dimension marshal', () => { populateMarshal(marshal); marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - // clearing initial calls - callbacks.publishDraggables.mockClear(); marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); // calls are batched - expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1); - const result: DraggableDimension[] = callbacks.publishDraggables.mock.calls[0][0]; + expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1); + const result: DraggableDimension[] = callbacks.bulkPublish.mock.calls[0][0]; + // not calling for the dragging item expect(result.length).toBe(Object.keys(preset.draggables).length - 1); + // super explicit test // - doing it like this because the order of Object.keys is not guarenteed Object.keys(preset.draggables).forEach((id: DraggableId) => { @@ -447,15 +455,13 @@ describe('dimension marshal', () => { populateMarshal(marshal); marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - // clearing initial calls - callbacks.publishDroppables.mockClear(); marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); // calls are batched - expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1); - const result: DroppableDimension[] = callbacks.publishDroppables.mock.calls[0][0]; + expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1); + const result: DroppableDimension[] = callbacks.bulkPublish.mock.calls[0][1]; // not calling for the dragging item expect(result.length).toBe(Object.keys(preset.droppables).length - 1); // super explicit test @@ -505,8 +511,13 @@ describe('dimension marshal', () => { marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); - expect(callbacks.publishDroppables.mock.calls[0][0]).not.toContain(ofAnotherType); - expect(callbacks.publishDraggables.mock.calls[0][0]).not.toContain(childOfAnotherType); + expect(callbacks.bulkPublish.mock.calls[0][0]).not.toContain(childOfAnotherType); + // validation + expect(callbacks.bulkPublish.mock.calls[0][0]).toContain(preset.inHome2); + + expect(callbacks.bulkPublish.mock.calls[0][1]).not.toContain(ofAnotherType); + // validation + expect(callbacks.bulkPublish.mock.calls[0][1]).toContain(preset.foreign); }); it('should not publish draggables if there are none to publish', () => { @@ -523,17 +534,14 @@ describe('dimension marshal', () => { marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); // asserting initial lift occurred - expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome1]); - expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.home]); - callbacks.publishDraggables.mockReset(); - callbacks.publishDroppables.mockReset(); + expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1); + expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home); // perform full lift marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); - expect(callbacks.publishDraggables).not.toHaveBeenCalled(); - expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.foreign]); + expect(callbacks.bulkPublish).toHaveBeenCalledWith([], [preset.foreign]); }); it('should not publish droppables if there are none to publish', () => { @@ -553,17 +561,14 @@ describe('dimension marshal', () => { marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); // asserting initial lift occurred - expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome1]); - expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.home]); - callbacks.publishDraggables.mockReset(); - callbacks.publishDroppables.mockReset(); + expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1); + expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home); // perform full lift marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); - expect(callbacks.publishDroppables).not.toHaveBeenCalled(); - expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome2]); + expect(callbacks.bulkPublish).toHaveBeenCalledWith([preset.inHome2], []); }); }); }); @@ -608,8 +613,9 @@ describe('dimension marshal', () => { let watchers; const resetMocks = () => { - callbacks.publishDraggables.mockClear(); - callbacks.publishDroppables.mockClear(); + callbacks.publishDraggable.mockClear(); + callbacks.publishDroppable.mockClear(); + callbacks.bulkPublish.mockClear(); watchers.draggable.getDimension.mockClear(); watchers.droppable.getDimension.mockClear(); watchers.droppable.watchScroll.mockClear(); @@ -623,10 +629,11 @@ describe('dimension marshal', () => { }); const shouldHaveProcessedInitialDimensions = (): void => { - expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.home]); - expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1); - expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome1]); - expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1); + expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home); + expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1); + expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1); + expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1); + expect(callbacks.bulkPublish).not.toHaveBeenCalled(); expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(1); expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(1); expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(preset.home.descriptor.id); @@ -635,8 +642,9 @@ describe('dimension marshal', () => { }; const shouldNotHavePublishedDimensions = (): void => { - expect(callbacks.publishDroppables).not.toHaveBeenCalled(); - expect(callbacks.publishDroppables).not.toHaveBeenCalled(); + expect(callbacks.publishDroppable).not.toHaveBeenCalled(); + expect(callbacks.publishDroppable).not.toHaveBeenCalled(); + expect(callbacks.bulkPublish).not.toHaveBeenCalled(); }; it('should support subsequent drags after a completed collection', () => { @@ -653,8 +661,9 @@ describe('dimension marshal', () => { marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); - expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1); - expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1); + expect(callbacks.publishDroppable).not.toHaveBeenCalled(); + expect(callbacks.publishDraggable).not.toHaveBeenCalled(); + expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1); expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(droppableCount - 1); expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(droppableCount - 1); expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(draggableCount - 1); @@ -732,6 +741,7 @@ describe('dimension marshal', () => { getDimension: () => preset.home, watchScroll: () => { }, unwatchScroll: () => { }, + scroll: () => {}, }; const getDraggableDimensionFn: GetDraggableDimensionFn = () => preset.inHome1; @@ -748,7 +758,7 @@ describe('dimension marshal', () => { marshal.registerDraggable(preset.inHome1.descriptor, getDraggableDimensionFn); marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.home]); + expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home); }); it('should overwrite an existing entry if needed', () => { @@ -759,7 +769,7 @@ describe('dimension marshal', () => { }); marshal.registerDroppable(preset.home.descriptor, droppableCallbacks); - const newDescriptor: DroppableDescriptor = { + const newHomeDescriptor: DroppableDescriptor = { id: preset.home.descriptor.id, type: preset.home.descriptor.type, }; @@ -767,13 +777,14 @@ describe('dimension marshal', () => { getDimension: () => preset.foreign, watchScroll: () => { }, unwatchScroll: () => { }, + scroll: () => { }, }; - marshal.registerDroppable(newDescriptor, newCallbacks); + marshal.registerDroppable(newHomeDescriptor, newCallbacks); marshal.registerDraggable(preset.inHome1.descriptor, getDraggableDimensionFn); marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - expect(callbacks.publishDroppables).toHaveBeenCalledWith([preset.foreign]); - expect(callbacks.publishDroppables).toHaveBeenCalledTimes(1); + expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.foreign); + expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1); }); }); @@ -796,7 +807,7 @@ describe('dimension marshal', () => { marshal.registerDraggable(preset.inHome1.descriptor, getDraggableDimensionFn); marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome1]); + expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1); }); it('should overwrite an existing entry if needed', () => { @@ -819,8 +830,8 @@ describe('dimension marshal', () => { marshal.registerDraggable(fake, () => preset.inHome2); marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - expect(callbacks.publishDraggables).toHaveBeenCalledTimes(1); - expect(callbacks.publishDraggables).toHaveBeenCalledWith([preset.inHome2]); + expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1); + expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome2); }); }); }); @@ -863,8 +874,6 @@ describe('dimension marshal', () => { // lift, collect and publish marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - // clearing state from original publish - callbacks.publishDroppables.mockClear(); // execute full lift marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); @@ -872,8 +881,10 @@ describe('dimension marshal', () => { expect(watchers.droppable.getDimension) .not.toHaveBeenCalledWith(preset.foreign.descriptor.id); - expect(callbacks.publishDroppables.mock.calls[0][0]) + expect(callbacks.bulkPublish.mock.calls[0][1]) .not.toContain(preset.foreign); + // validation + expect(callbacks.bulkPublish.mock.calls[0][1]).toContain(preset.emptyForeign); // checking we are not causing an orphan child warning expect(console.warn).not.toHaveBeenCalled(); @@ -916,6 +927,7 @@ describe('dimension marshal', () => { getDimension: getOldDimension, watchScroll: () => { }, unwatchScroll: () => { }, + scroll: () => { }, }); marshal.registerDraggable(preset.inHome1.descriptor, () => preset.inHome1); @@ -934,6 +946,7 @@ describe('dimension marshal', () => { getDimension: getNewDimension, watchScroll: () => { }, unwatchScroll: () => { }, + scroll: () => { }, }); // perform full lift @@ -1035,11 +1048,11 @@ describe('dimension marshal', () => { // start a collection marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - callbacks.publishDraggables.mockReset(); + callbacks.publishDraggable.mockReset(); // now registering marshal.registerDraggable(fake.descriptor, () => fake); - expect(callbacks.publishDraggables).not.toHaveBeenCalled(); + expect(callbacks.publishDraggable).not.toHaveBeenCalled(); expect(console.warn).toHaveBeenCalled(); }); }); @@ -1064,14 +1077,14 @@ describe('dimension marshal', () => { // starting collection marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - callbacks.publishDroppables.mockReset(); + callbacks.publishDroppable.mockReset(); // updating registration marshal.registerDroppable(fake.descriptor, droppableCallbacks); // warning should have been logged and nothing updated expect(console.warn).toHaveBeenCalled(); - expect(callbacks.publishDroppables).not.toHaveBeenCalled(); + expect(callbacks.publishDroppable).not.toHaveBeenCalled(); expect(droppableCallbacks.watchScroll).not.toHaveBeenCalled(); }); }); diff --git a/test/utils/dimension.js b/test/utils/dimension.js index 78ffe79ca8..a7e67bf2bc 100644 --- a/test/utils/dimension.js +++ b/test/utils/dimension.js @@ -62,8 +62,6 @@ export const makeScrollable = (droppable: DroppableDimension, amount?: number = } export const getPreset = (axis?: Axis = vertical) => { - - const home: DroppableDimension = getDroppableDimension({ descriptor: { id: 'home', From 095e109d418ea0636a69df7b4a68b14cfec4f89e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 12:57:36 +1100 Subject: [PATCH 113/163] reordering bulk publish params --- src/state/action-creators.js | 9 ++-- .../dimension-marshal-types.js | 2 +- .../dimension-marshal/dimension-marshal.js | 4 +- src/state/reducer.js | 1 - .../drag-drop-context/drag-drop-context.jsx | 4 +- .../lift-action-and-dimension-marshal.spec.js | 4 +- test/unit/view/dimension-marshal.spec.js | 50 ++++++++++--------- 7 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index c5acaa40d1..f66eda6b59 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -39,7 +39,7 @@ const getScrollDiff = ({ current.windowScroll ); - if(!droppable) { + if (!droppable) { return windowScrollDiff; } @@ -106,18 +106,19 @@ export const publishDroppableDimension = export type BulkPublishDimensionsAction = {| type: 'BULK_DIMENSION_PUBLISH', payload: {| - draggables: DraggableDimension[], droppables: DroppableDimension[], + draggables: DraggableDimension[], |} |} export const bulkPublishDimensions = ( + droppables: DroppableDimension[], draggables: DraggableDimension[], - droppables: DroppableDimension[] ): BulkPublishDimensionsAction => ({ type: 'BULK_DIMENSION_PUBLISH', payload: { - draggables, droppables, + droppables, + draggables, }, }); diff --git a/src/state/dimension-marshal/dimension-marshal-types.js b/src/state/dimension-marshal/dimension-marshal-types.js index 68c8237eb9..aaab09a4e9 100644 --- a/src/state/dimension-marshal/dimension-marshal-types.js +++ b/src/state/dimension-marshal/dimension-marshal-types.js @@ -69,8 +69,8 @@ export type Callbacks = {| publishDraggable: (DraggableDimension) => void, publishDroppable: (DroppableDimension) => void, bulkPublish: ( - draggables: DraggableDimension[], droppables: DroppableDimension[], + draggables: DraggableDimension[], ) => void, updateDroppableScroll: (id: DroppableId, newScroll: Position) => void, updateDroppableIsEnabled: (id: DroppableId, isEnabled: boolean) => void, diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index 7c3b37485f..6268d9626f 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -33,8 +33,8 @@ type State = {| |} type ToBePublished = {| - draggables: DraggableDimension[], droppables: DroppableDimension[], + draggables: DraggableDimension[], |} export default (callbacks: Callbacks) => { @@ -369,8 +369,8 @@ export default (callbacks: Callbacks) => { ); callbacks.bulkPublish( - toBePublished.draggables, toBePublished.droppables, + toBePublished.draggables, ); // need to watch the scroll on each droppable diff --git a/src/state/reducer.js b/src/state/reducer.js index 8a9c4409ce..56e0f4ad65 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -463,7 +463,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { const impact: ?DragImpact = (() => { // we do not want to recalculate the initial impact until the first bulk publish is finished if (!drag.current.hasCompletedFirstBulkPublish) { - console.log('have not completed first bulk publish'); return drag.impact; } diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index e8751e77d0..e0cd6fa431 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -118,8 +118,8 @@ export default class DragDropContext extends React.Component { publishDroppable: (dimension: DroppableDimension) => { this.store.dispatch(publishDroppableDimension(dimension)); }, - bulkPublish: (draggables: DraggableDimension[], droppables: DroppableDimension[]) => { - this.store.dispatch(bulkPublishDimensions(draggables, droppables)); + bulkPublish: (droppables: DroppableDimension[], draggables: DraggableDimension[]) => { + this.store.dispatch(bulkPublishDimensions(droppables, draggables)); }, updateDroppableScroll: (id: DroppableId, newScroll: Position) => { this.store.dispatch(updateDroppableDimensionScroll(id, newScroll)); diff --git a/test/unit/integration/lift-action-and-dimension-marshal.spec.js b/test/unit/integration/lift-action-and-dimension-marshal.spec.js index 4dbe12b285..2baa2faa20 100644 --- a/test/unit/integration/lift-action-and-dimension-marshal.spec.js +++ b/test/unit/integration/lift-action-and-dimension-marshal.spec.js @@ -42,8 +42,8 @@ const getDimensionMarshal = (store: Store): DimensionMarshal => { publishDroppable: (dimension: DroppableDimension) => { store.dispatch(publishDroppableDimension(dimension)); }, - bulkPublish: (draggables: DraggableDimension[], droppables: DroppableDimension[]) => { - store.dispatch(bulkPublishDimensions(draggables, droppables)); + bulkPublish: (droppables: DroppableDimension[], draggables: DraggableDimension[]) => { + store.dispatch(bulkPublishDimensions(droppables, draggables)); }, updateDroppableScroll: (id: DroppableId, offset: Position) => { store.dispatch(updateDroppableDimensionScroll(id, offset)); diff --git a/test/unit/view/dimension-marshal.spec.js b/test/unit/view/dimension-marshal.spec.js index 08c8458461..5f3497cf17 100644 --- a/test/unit/view/dimension-marshal.spec.js +++ b/test/unit/view/dimension-marshal.spec.js @@ -421,7 +421,7 @@ describe('dimension marshal', () => { expect(callbacks.bulkPublish).not.toHaveBeenCalled(); }); - it('should publish all the collected draggables', () => { + it('should publish all the collected droppables', () => { const callbacks = getCallbackStub(); const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); @@ -433,23 +433,21 @@ describe('dimension marshal', () => { // calls are batched expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1); - const result: DraggableDimension[] = callbacks.bulkPublish.mock.calls[0][0]; - + const result: DroppableDimension[] = callbacks.bulkPublish.mock.calls[0][0]; // not calling for the dragging item - expect(result.length).toBe(Object.keys(preset.draggables).length - 1); - + expect(result.length).toBe(Object.keys(preset.droppables).length - 1); // super explicit test // - doing it like this because the order of Object.keys is not guarenteed - Object.keys(preset.draggables).forEach((id: DraggableId) => { - if (id === preset.inHome1.descriptor.id) { - expect(result).not.toContain(preset.inHome1); + Object.keys(preset.droppables).forEach((id: DroppableId) => { + if (id === preset.home.descriptor.id) { + expect(result.includes(preset.home)).toBe(false); return; } - expect(result).toContain(preset.draggables[id]); + expect(result.includes(preset.droppables[id])).toBe(true); }); }); - it('should publish all the collected droppables', () => { + it('should publish all the collected draggables', () => { const callbacks = getCallbackStub(); const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); @@ -461,17 +459,19 @@ describe('dimension marshal', () => { // calls are batched expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1); - const result: DroppableDimension[] = callbacks.bulkPublish.mock.calls[0][1]; + const result: DraggableDimension[] = callbacks.bulkPublish.mock.calls[0][1]; + // not calling for the dragging item - expect(result.length).toBe(Object.keys(preset.droppables).length - 1); + expect(result.length).toBe(Object.keys(preset.draggables).length - 1); + // super explicit test // - doing it like this because the order of Object.keys is not guarenteed - Object.keys(preset.droppables).forEach((id: DroppableId) => { - if (id === preset.home.descriptor.id) { - expect(result.includes(preset.home)).toBe(false); + Object.keys(preset.draggables).forEach((id: DraggableId) => { + if (id === preset.inHome1.descriptor.id) { + expect(result).not.toContain(preset.inHome1); return; } - expect(result.includes(preset.droppables[id])).toBe(true); + expect(result).toContain(preset.draggables[id]); }); }); @@ -511,13 +511,13 @@ describe('dimension marshal', () => { marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); - expect(callbacks.bulkPublish.mock.calls[0][0]).not.toContain(childOfAnotherType); + expect(callbacks.bulkPublish.mock.calls[0][0]).not.toContain(ofAnotherType); // validation - expect(callbacks.bulkPublish.mock.calls[0][0]).toContain(preset.inHome2); + expect(callbacks.bulkPublish.mock.calls[0][0]).toContain(preset.foreign); - expect(callbacks.bulkPublish.mock.calls[0][1]).not.toContain(ofAnotherType); + expect(callbacks.bulkPublish.mock.calls[0][1]).not.toContain(childOfAnotherType); // validation - expect(callbacks.bulkPublish.mock.calls[0][1]).toContain(preset.foreign); + expect(callbacks.bulkPublish.mock.calls[0][1]).toContain(preset.inHome2); }); it('should not publish draggables if there are none to publish', () => { @@ -541,7 +541,7 @@ describe('dimension marshal', () => { marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); - expect(callbacks.bulkPublish).toHaveBeenCalledWith([], [preset.foreign]); + expect(callbacks.bulkPublish).toHaveBeenCalledWith([preset.foreign], []); }); it('should not publish droppables if there are none to publish', () => { @@ -568,7 +568,7 @@ describe('dimension marshal', () => { marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); - expect(callbacks.bulkPublish).toHaveBeenCalledWith([preset.inHome2], []); + expect(callbacks.bulkPublish).toHaveBeenCalledWith([], [preset.inHome2]); }); }); }); @@ -881,10 +881,10 @@ describe('dimension marshal', () => { expect(watchers.droppable.getDimension) .not.toHaveBeenCalledWith(preset.foreign.descriptor.id); - expect(callbacks.bulkPublish.mock.calls[0][1]) + expect(callbacks.bulkPublish.mock.calls[0][0]) .not.toContain(preset.foreign); // validation - expect(callbacks.bulkPublish.mock.calls[0][1]).toContain(preset.emptyForeign); + expect(callbacks.bulkPublish.mock.calls[0][0]).toContain(preset.emptyForeign); // checking we are not causing an orphan child warning expect(console.warn).not.toHaveBeenCalled(); @@ -990,6 +990,7 @@ describe('dimension marshal', () => { getDimension: () => preset.home, watchScroll: () => {}, unwatchScroll: () => {}, + scroll: () => {}, }); const getOldDimension: GetDraggableDimensionFn = jest.fn().mockImplementation(() => preset.inHome2); @@ -1073,6 +1074,7 @@ describe('dimension marshal', () => { getDimension: () => fake, watchScroll: jest.fn(), unwatchScroll: () => { }, + scroll: () => {}, }; // starting collection From 376ab17af8f7083afc249fed326b635b86a2a726 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 14:37:52 +1100 Subject: [PATCH 114/163] progress --- src/state/dimension.js | 40 +++++------ src/state/move-to-next-index/in-home-list.js | 3 +- test/unit/state/move-to-next-index.spec.js | 74 +++++++++++++++++--- 3 files changed, 85 insertions(+), 32 deletions(-) diff --git a/src/state/dimension.js b/src/state/dimension.js index 8ce95f47d6..7c16834977 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -65,26 +65,6 @@ export const getDraggableDimension = ({ return dimension; }; -type GetDroppableArgs = {| - descriptor: DroppableDescriptor, - client: Area, - // optionally provided - and can also be null - closest?: ?{| - frameClient: Area, - scrollWidth: number, - scrollHeight: number, - scroll: Position, - shouldClipSubject: boolean, - |}, - direction?: Direction, - margin?: Spacing, - padding?: Spacing, - windowScroll?: Position, - // Whether or not the droppable is currently enabled (can change at during a drag) - // defaults to true - isEnabled?: boolean, -|} - // will return null if the subject is completely not visible within frame export const clip = (frame: Area, subject: Spacing): ?Area => { const result: Area = getArea({ @@ -153,6 +133,26 @@ export const scrollDroppable = ( }; }; +type GetDroppableArgs = {| + descriptor: DroppableDescriptor, + client: Area, + // optionally provided - and can also be null + closest?: ?{| + frameClient: Area, + scrollWidth: number, + scrollHeight: number, + scroll: Position, + shouldClipSubject: boolean, + |}, + direction?: Direction, + margin?: Spacing, + padding?: Spacing, + windowScroll?: Position, + // Whether or not the droppable is currently enabled (can change at during a drag) + // defaults to true + isEnabled?: boolean, +|} + export const getDroppableDimension = ({ descriptor, client, diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index a178c1865c..32b5a18fb9 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -98,12 +98,11 @@ export default ({ [destinationDisplacement, ...previousImpact.movement.displaced]); // update impact with visibility - stops redundant work! - const displaced: Displacement[] = modified .map((displacement: Displacement): Displacement => { const target: DraggableDimension = draggables[displacement.draggableId]; - // TODO: the visibility post drag might be different to this! + // TODO: need to adjust target so that it is in the new location const updated: Displacement = getDisplacement({ draggable: target, destination: droppable, diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index 8b44dff65e..9e8d3eb506 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -4,7 +4,7 @@ import type { Result } from '../../../src/state/move-to-next-index/move-to-next- import { getPreset, disableDroppable } from '../../utils/dimension'; import moveToEdge from '../../../src/state/move-to-edge'; import noImpact, { noMovement } from '../../../src/state/no-impact'; -import { patch } from '../../../src/state/position'; +import { patch, subtract } from '../../../src/state/position'; import { vertical, horizontal } from '../../../src/state/axis'; import getViewport from '../../../src/window/get-viewport'; import getArea from '../../../src/state/get-area'; @@ -61,6 +61,7 @@ describe('move to next index', () => { const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: preset.inHome1.descriptor.id, + previousPageCenter: preset.inHome1.page.withoutMargin.center, previousImpact: noImpact, droppable: disabled, draggables: preset.draggables, @@ -69,7 +70,7 @@ describe('move to next index', () => { expect(result).toEqual(null); }); - describe('in home list', () => { + describe.only('in home list', () => { describe('moving forwards', () => { it('should return null if cannot move forward', () => { const previousImpact: DragImpact = { @@ -87,6 +88,7 @@ describe('move to next index', () => { isMovingForward: true, draggableId: preset.inHome3.descriptor.id, previousImpact, + previousPageCenter: preset.inHome3.page.withoutMargin.center, droppable: preset.home, draggables: preset.draggables, }); @@ -110,6 +112,7 @@ describe('move to next index', () => { isMovingForward: true, draggableId: preset.inHome1.descriptor.id, previousImpact, + previousPageCenter: preset.inHome1.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -167,6 +170,7 @@ describe('move to next index', () => { isMovingForward: true, draggableId: preset.inHome2.descriptor.id, previousImpact, + previousPageCenter: preset.inHome2.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -229,10 +233,14 @@ describe('move to next index', () => { index: 1, }, }; + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: preset.inHome1.descriptor.id, previousImpact, + // roughly correct previous page center + // not calculating the exact point as it is not required for this test + previousPageCenter: preset.inHome2.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -311,6 +319,8 @@ describe('move to next index', () => { isMovingForward: true, draggableId: preset.inHome2.descriptor.id, previousImpact, + // roughly correct: + previousPageCenter: preset.inHome1.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -384,6 +394,8 @@ describe('move to next index', () => { isMovingForward: true, draggableId: preset.inHome3.descriptor.id, previousImpact, + // this is roughly correct + previousPageCenter: preset.inHome1.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -449,6 +461,7 @@ describe('move to next index', () => { isMovingForward: false, draggableId: preset.inHome1.descriptor.id, previousImpact, + previousPageCenter: preset.inHome1.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -475,6 +488,7 @@ describe('move to next index', () => { isMovingForward: false, draggableId: preset.inHome2.descriptor.id, previousImpact, + previousPageCenter: preset.inHome2.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -535,6 +549,7 @@ describe('move to next index', () => { isMovingForward: false, draggableId: preset.inHome3.descriptor.id, previousImpact, + previousPageCenter: preset.inHome3.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -603,6 +618,8 @@ describe('move to next index', () => { isMovingForward: false, draggableId: preset.inHome2.descriptor.id, previousImpact, + // roughly correct + previousPageCenter: preset.inHome3.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -676,6 +693,8 @@ describe('move to next index', () => { isMovingForward: false, draggableId: preset.inHome1.descriptor.id, previousImpact, + // roughly correct + previousPageCenter: preset.inHome3.page.withoutMargin.center, draggables: preset.draggables, droppable: preset.home, }); @@ -736,7 +755,7 @@ describe('move to next index', () => { }), direction: axis.direction, }); - const inViewport: DraggableDimension = getDraggableDimension({ + const asBigAsViewport: DraggableDimension = getDraggableDimension({ descriptor: { id: 'inside', index: 0, @@ -768,27 +787,62 @@ describe('move to next index', () => { }, }; const draggables: DraggableDimensionMap = { - [inViewport.descriptor.id]: inViewport, + [asBigAsViewport.descriptor.id]: asBigAsViewport, [outsideViewport.descriptor.id]: outsideViewport, }; - it('should not permit movement into areas that are outside the viewport', () => { + it.only('should request a jump scroll for movement that is outside of the viewport', () => { + const expectedCenter = moveToEdge({ + source: asBigAsViewport.page.withoutMargin, + sourceEdge: 'end', + destination: outsideViewport.page.withMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + const previousPageCenter: Position = asBigAsViewport.page.withoutMargin.center; + const expectedScrollJump: Position = subtract(expectedCenter, previousPageCenter); + const expectedImpact: DragImpact = { + movement: { + displaced: [{ + draggableId: outsideViewport.descriptor.id, + // as the item started in an invisible place + isVisible: true, + shouldAnimate: true, + }], + amount: patch(axis.line, asBigAsViewport.page.withMargin[axis.size]), + isBeyondStartPosition: true, + }, + destination: { + droppableId: droppable.descriptor.id, + index: 1, + }, + direction: axis.direction, + }; + const result: ?Result = moveToNextIndex({ isMovingForward: true, - draggableId: inViewport.descriptor.id, + draggableId: asBigAsViewport.descriptor.id, previousImpact, + previousPageCenter, draggables, droppable, }); - expect(result).toBe(null); + if (!result) { + throw new Error('Invalid test setup'); + } + + // not updating the page center (visually the item will not move) + expect(result.pageCenter).toEqual(previousPageCenter); + expect(result.scrollJumpRequest).toEqual(expectedScrollJump); + expect(result.impact).toEqual(expectedImpact); }); it('should take into account any changes in the droppables scroll', () => { // scrolling so that outsideViewport is now visible setWindowScroll({ x: 200, y: 200 }); const expectedCenter = moveToEdge({ - source: inViewport.page.withoutMargin, + source: asBigAsViewport.page.withoutMargin, sourceEdge: 'end', destination: outsideViewport.page.withMargin, destinationEdge: 'end', @@ -801,7 +855,7 @@ describe('move to next index', () => { isVisible: true, shouldAnimate: true, }], - amount: patch(axis.line, inViewport.page.withMargin[axis.size]), + amount: patch(axis.line, asBigAsViewport.page.withMargin[axis.size]), isBeyondStartPosition: true, }, destination: { @@ -813,7 +867,7 @@ describe('move to next index', () => { const result: ?Result = moveToNextIndex({ isMovingForward: true, - draggableId: inViewport.descriptor.id, + draggableId: asBigAsViewport.descriptor.id, previousImpact, draggables, droppable, From 7e5148ea70c88227969e1cd6fcb5cce06df4bb7b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 15:46:25 +1100 Subject: [PATCH 115/163] progress --- src/state/move-to-next-index/in-home-list.js | 3 ++- stories/3-board-story.js | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 32b5a18fb9..caf5abc336 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,12 +1,13 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch, subtract } from '../position'; +import { patch, subtract, negate } from '../position'; import withDroppableDisplacement from '../with-droppable-displacement'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import getViewport from '../../window/get-viewport'; // import getScrollJumpResult from './get-scroll-jump-result'; import moveToEdge from '../move-to-edge'; import getDisplacement from '../get-displacement'; +import { shiftDraggable } from '../dimension'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import type { diff --git a/stories/3-board-story.js b/stories/3-board-story.js index 2377d2090c..663135c70d 100644 --- a/stories/3-board-story.js +++ b/stories/3-board-story.js @@ -13,10 +13,12 @@ storiesOf('board', module) .add('simple', () => ( )) - // TODO: revert to large - .add('large data set', () => ( + .add('medium data set', () => ( )) + .add('large data set', () => ( + + )) .add('long lists in a short container', () => ( )); From 6d6d2e2761beed90a04d20bfe0846628115c332b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 16:56:44 +1100 Subject: [PATCH 116/163] fixing visible edge displacement --- src/state/get-displacement.js | 3 +++ src/state/move-to-next-index/in-home-list.js | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/state/get-displacement.js b/src/state/get-displacement.js index 80d7e16b0e..122eb140d6 100644 --- a/src/state/get-displacement.js +++ b/src/state/get-displacement.js @@ -17,6 +17,9 @@ type Args = {| viewport: Area, |} +// Note: it is also an optimisation to undo the displacement on items when they are no longer visible. +// This prevents a lot of .render() calls when leaving a list + export default ({ draggable, destination, diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index caf5abc336..2a4f6eb6cd 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -7,7 +7,6 @@ import getViewport from '../../window/get-viewport'; // import getScrollJumpResult from './get-scroll-jump-result'; import moveToEdge from '../move-to-edge'; import getDisplacement from '../get-displacement'; -import { shiftDraggable } from '../dimension'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import type { @@ -101,11 +100,13 @@ export default ({ // update impact with visibility - stops redundant work! const displaced: Displacement[] = modified .map((displacement: Displacement): Displacement => { - const target: DraggableDimension = draggables[displacement.draggableId]; + // we have already calculated the displacement for this item + if (displacement === destinationDisplacement) { + return displacement; + } - // TODO: need to adjust target so that it is in the new location const updated: Displacement = getDisplacement({ - draggable: target, + draggable: draggables[displacement.draggableId], destination: droppable, previousImpact, viewport, From 4bccd136abc8b232c7261a6863b04de2ef7d3e38 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 17:36:32 +1100 Subject: [PATCH 117/163] cleaning up comment --- src/state/move-to-next-index/in-home-list.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 2a4f6eb6cd..d90de8276a 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -83,8 +83,7 @@ export default ({ destinationAxis: droppable.axis, }); - // Calculate DragImpact - // at this point we know that the destination is droppable + // As this is a forced displacement we always want it to be visible and animate const destinationDisplacement: Displacement = { draggableId: destination.descriptor.id, isVisible: true, From 5af81b55df08d06143c76e5a8c44cf76f39827ea Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 13 Feb 2018 17:54:51 +1100 Subject: [PATCH 118/163] correcting partial scrolling algorithm --- src/state/auto-scroller/can-scroll.js | 18 ++++- .../unit/state/auto-scroll/can-scroll.spec.js | 68 +++++++++---------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/state/auto-scroller/can-scroll.js b/src/state/auto-scroller/can-scroll.js index 0752e92ea2..f6c281c165 100644 --- a/src/state/auto-scroller/can-scroll.js +++ b/src/state/auto-scroller/can-scroll.js @@ -78,8 +78,22 @@ export const canPartiallyScroll = ({ max, current, change: smallestChange, }); - // there will be no remainder if you can partially scroll - return !overlap; + // no overlap at all - we can move there! + if (!overlap) { + return true; + } + + // if there was an x value, but there is no x overlap - then we can scroll on the x! + if (smallestChange.x !== 0 && overlap.x === 0) { + return true; + } + + // if there was an y value, but there is no y overlap - then we can scroll on the y! + if (smallestChange.y !== 0 && overlap.y === 0) { + return true; + } + + return false; }; const getMaxWindowScroll = (): Position => { diff --git a/test/unit/state/auto-scroll/can-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js index 5907347a53..75ad679d95 100644 --- a/test/unit/state/auto-scroll/can-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-scroll.spec.js @@ -152,46 +152,46 @@ describe('can scroll', () => { }); }); - it('should return false if can only scroll in one direction', () => { - const max: Position = { x: 100, y: 200 }; - type Item = {| current: Position, change: Position, |} - const changes: Item[] = [ - // Can move back in the y direction, but not back in the x direction - { - current: { x: 0, y: 1 }, - change: { x: -1, y: -1 }, - }, - // Can move back in the x direction, but not back in the y direction - { - current: { x: 1, y: 0 }, - change: { x: -1, y: -1 }, - }, - // Can move forward in the y direction, but not forward in the x direction - { - current: subtract(max, { x: 0, y: 1 }), - change: { x: 1, y: 1 }, - }, - // Can move forward in the x direction, but not forward in the y direction - { - current: subtract(max, { x: 1, y: 0 }), - change: { x: 1, y: 1 }, - }, - ]; - - changes.forEach((item: Item) => { - const result: boolean = canPartiallyScroll({ - max, - current: item.current, - change: item.change, - }); + it('should return true if can only partially move in one direction', () => { + const max: Position = { x: 100, y: 200 }; - expect(result).toBe(false); - }); + const changes: Item[] = [ + // Can move back in the y direction, but not back in the x direction + { + current: { x: 0, y: 1 }, + change: { x: -1, y: -1 }, + }, + // Can move back in the x direction, but not back in the y direction + { + current: { x: 1, y: 0 }, + change: { x: -1, y: -1 }, + }, + // Can move forward in the y direction, but not forward in the x direction + { + current: subtract(max, { x: 0, y: 1 }), + change: { x: 1, y: 1 }, + }, + // Can move forward in the x direction, but not forward in the y direction + { + current: subtract(max, { x: 1, y: 0 }), + change: { x: 1, y: 1 }, + }, + ]; + + changes.forEach((item: Item) => { + const result: boolean = canPartiallyScroll({ + max, + current: item.current, + change: item.change, + }); + + expect(result).toBe(true); + }); }); it('should return false if on the min point and move backward in any direction', () => { From 3cf79049c25fa0cb16b36e2bdf5a4e929a8da8c2 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 14 Feb 2018 08:36:00 +1100 Subject: [PATCH 119/163] fixing move-to-next-index tests --- src/state/get-drag-impact/in-home-list.js | 2 +- test/unit/state/move-to-next-index.spec.js | 199 ++++++++++++++------- 2 files changed, 137 insertions(+), 64 deletions(-) diff --git a/src/state/get-drag-impact/in-home-list.js b/src/state/get-drag-impact/in-home-list.js index e90d52e061..fafaebb643 100644 --- a/src/state/get-drag-impact/in-home-list.js +++ b/src/state/get-drag-impact/in-home-list.js @@ -37,8 +37,8 @@ export default ({ // The starting center position const originalCenter: Position = draggable.page.withoutMargin.center; + // Where the element actually is now. // Need to take into account the change of scroll in the droppable - // Where the element actually is now const currentCenter: Position = withDroppableScroll(home, pageCenter); // not considering margin so that items move based on visible edges diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index 9e8d3eb506..ead91100dd 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -70,7 +70,7 @@ describe('move to next index', () => { expect(result).toEqual(null); }); - describe.only('in home list', () => { + describe('in home list', () => { describe('moving forwards', () => { it('should return null if cannot move forward', () => { const previousImpact: DragImpact = { @@ -747,51 +747,51 @@ describe('move to next index', () => { id: 'much bigger than viewport', type: 'huge', }, + direction: axis.direction, client: getArea({ top: 0, right: 10000, bottom: 10000, left: 0, }), - direction: axis.direction, - }); - const asBigAsViewport: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inside', - index: 0, - droppableId: droppable.descriptor.id, - }, - client: customViewport, }); - const outsideViewport: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'outside', - index: 1, - droppableId: droppable.descriptor.id, - }, - client: getArea({ - // is bottom left of the viewport - top: customViewport.bottom + 1, - right: customViewport.right + 100, - left: customViewport.right + 1, - bottom: customViewport.bottom + 100, - }), - }); - // inViewport is in its original position - const previousImpact: DragImpact = { - movement: noMovement, - direction: axis.direction, - destination: { - index: 0, - droppableId: droppable.descriptor.id, - }, - }; - const draggables: DraggableDimensionMap = { - [asBigAsViewport.descriptor.id]: asBigAsViewport, - [outsideViewport.descriptor.id]: outsideViewport, - }; - it.only('should request a jump scroll for movement that is outside of the viewport', () => { + it('should request a jump scroll for movement that is outside of the viewport', () => { + const asBigAsViewport: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inside', + index: 0, + droppableId: droppable.descriptor.id, + }, + client: customViewport, + }); + const outsideViewport: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'outside', + index: 1, + droppableId: droppable.descriptor.id, + }, + client: getArea({ + // is bottom left of the viewport + top: customViewport.bottom + 1, + right: customViewport.right + 100, + left: customViewport.right + 1, + bottom: customViewport.bottom + 100, + }), + }); + // inViewport is in its original position + const previousImpact: DragImpact = { + movement: noMovement, + direction: axis.direction, + destination: { + index: 0, + droppableId: droppable.descriptor.id, + }, + }; + const draggables: DraggableDimensionMap = { + [asBigAsViewport.descriptor.id]: asBigAsViewport, + [outsideViewport.descriptor.id]: outsideViewport, + }; const expectedCenter = moveToEdge({ source: asBigAsViewport.page.withoutMargin, sourceEdge: 'end', @@ -805,7 +805,8 @@ describe('move to next index', () => { movement: { displaced: [{ draggableId: outsideViewport.descriptor.id, - // as the item started in an invisible place + // Even though the item started in an invisible place we force + // the displacement to be visible. isVisible: true, shouldAnimate: true, }], @@ -838,24 +839,57 @@ describe('move to next index', () => { expect(result.impact).toEqual(expectedImpact); }); - it('should take into account any changes in the droppables scroll', () => { - // scrolling so that outsideViewport is now visible - setWindowScroll({ x: 200, y: 200 }); - const expectedCenter = moveToEdge({ - source: asBigAsViewport.page.withoutMargin, - sourceEdge: 'end', - destination: outsideViewport.page.withMargin, - destinationEdge: 'end', - destinationAxis: axis, + it('should force visible displacement when displacing an invisible item', () => { + const visible: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inside', + index: 0, + droppableId: droppable.descriptor.id, + }, + client: getArea({ + top: 0, + left: 0, + right: customViewport.right - 100, + bottom: customViewport.bottom - 100, + }), }); + const invisible: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'partial', + index: 1, + droppableId: droppable.descriptor.id, + }, + client: getArea({ + top: customViewport.bottom + 1, + left: customViewport.right + 1, + bottom: customViewport.bottom + 100, + right: customViewport.right + 100, + }), + }); + // inViewport is in its original position + const previousImpact: DragImpact = { + movement: noMovement, + direction: axis.direction, + destination: { + index: 0, + droppableId: droppable.descriptor.id, + }, + }; + const draggables: DraggableDimensionMap = { + [visible.descriptor.id]: visible, + [invisible.descriptor.id]: invisible, + }; + const previousPageCenter: Position = visible.page.withoutMargin.center; const expectedImpact: DragImpact = { movement: { displaced: [{ - draggableId: outsideViewport.descriptor.id, + draggableId: invisible.descriptor.id, + // Even though the item started in an invisible place we force + // the displacement to be visible. isVisible: true, shouldAnimate: true, }], - amount: patch(axis.line, asBigAsViewport.page.withMargin[axis.size]), + amount: patch(axis.line, visible.page.withMargin[axis.size]), isBeyondStartPosition: true, }, destination: { @@ -867,28 +901,29 @@ describe('move to next index', () => { const result: ?Result = moveToNextIndex({ isMovingForward: true, - draggableId: asBigAsViewport.descriptor.id, + draggableId: visible.descriptor.id, previousImpact, + previousPageCenter, draggables, droppable, }); if (!result) { - throw new Error('invalid result'); + throw new Error('Invalid test setup'); } - expect(result.pageCenter).toEqual(expectedCenter); expect(result.impact).toEqual(expectedImpact); }); }); describe('droppable visibility', () => { - it('should not permit movement into areas that outside of the droppable frame', () => { + it('should request a scroll jump into non-visible areas', () => { const droppable: DroppableDimension = getDroppableDimension({ descriptor: { id: 'much bigger than viewport', type: 'huge', }, + direction: axis.direction, client: getArea({ top: 0, left: 0, @@ -896,13 +931,18 @@ describe('move to next index', () => { bottom: 200, right: 200, }), - frameClient: getArea({ - top: 0, - left: 0, - right: 100, - bottom: 100, - }), - direction: axis.direction, + closest: { + frameClient: getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + scrollHeight: 200, + scrollWidth: 200, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const inside: DraggableDimension = getDraggableDimension({ descriptor: { @@ -932,7 +972,6 @@ describe('move to next index', () => { bottom: 180, }), }); - // inViewport is in its original position const previousImpact: DragImpact = { movement: noMovement, direction: axis.direction, @@ -945,16 +984,50 @@ describe('move to next index', () => { [inside.descriptor.id]: inside, [outside.descriptor.id]: outside, }; + const previousPageCenter: Position = inside.page.withoutMargin.center; + const expectedCenter = moveToEdge({ + source: inside.page.withoutMargin, + sourceEdge: 'end', + destination: outside.page.withMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + const expectedScrollJump: Position = subtract(expectedCenter, previousPageCenter); + const expectedImpact: DragImpact = { + movement: { + displaced: [{ + draggableId: outside.descriptor.id, + // Even though the item started in an invisible place we force + // the displacement to be visible. + isVisible: true, + shouldAnimate: true, + }], + amount: patch(axis.line, inside.page.withMargin[axis.size]), + isBeyondStartPosition: true, + }, + destination: { + droppableId: droppable.descriptor.id, + index: 1, + }, + direction: axis.direction, + }; const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: inside.descriptor.id, previousImpact, + previousPageCenter, draggables, droppable, }); - expect(result).toBe(null); + if (!result) { + throw new Error('Invalid test setup'); + } + + expect(result.pageCenter).toEqual(previousPageCenter); + expect(result.impact).toEqual(expectedImpact); + expect(result.scrollJumpRequest).toEqual(expectedScrollJump); }); it.skip('should take into account any changes in the droppables scroll', () => { From 85816c53f72776fa3274d085169266270504d6e9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 14 Feb 2018 08:39:53 +1100 Subject: [PATCH 120/163] fixing flow --- test/unit/state/move-to-next-index.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index ead91100dd..9e64d3db0f 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -1078,6 +1078,7 @@ describe('move to next index', () => { isMovingForward: true, draggableId: preset.inHome1.descriptor.id, previousImpact, + previousPageCenter: preset.inHome1.page.withoutMargin.center, droppable: preset.foreign, draggables: preset.draggables, }); @@ -1153,6 +1154,7 @@ describe('move to next index', () => { isMovingForward: true, draggableId: preset.inHome1.descriptor.id, previousImpact, + previousPageCenter: preset.inHome1.page.withoutMargin.center, droppable: preset.foreign, draggables: preset.draggables, }); @@ -1212,6 +1214,8 @@ describe('move to next index', () => { isMovingForward: true, draggableId: preset.inHome1.descriptor.id, previousImpact, + // roughly correct + previousPageCenter: preset.inHome4.page.withoutMargin.center, droppable: preset.foreign, draggables: preset.draggables, }); @@ -1265,6 +1269,8 @@ describe('move to next index', () => { isMovingForward: false, draggableId: preset.inHome1.descriptor.id, previousImpact, + // roughly correct + previousPageCenter: preset.inForeign1.page.withoutMargin.center, droppable: preset.foreign, draggables: preset.draggables, }); @@ -1296,6 +1302,8 @@ describe('move to next index', () => { isMovingForward: false, draggableId: preset.inHome1.descriptor.id, previousImpact, + // roughly correct + previousPageCenter: preset.inForeign4.page.withoutMargin.center, droppable: preset.foreign, draggables: preset.draggables, }); @@ -1383,6 +1391,8 @@ describe('move to next index', () => { isMovingForward: false, draggableId: preset.inHome1.descriptor.id, previousImpact, + // roughly correct + previousPageCenter: preset.inForeign2.page.withoutMargin.center, droppable: preset.foreign, draggables: preset.draggables, }); From 8727a3cfe71c53d91bbbe376e22db2b85119e292 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 14 Feb 2018 10:09:33 +1100 Subject: [PATCH 121/163] fixing tests for canscroll --- .../unit/state/auto-scroll/can-scroll.spec.js | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/test/unit/state/auto-scroll/can-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js index 75ad679d95..291a041420 100644 --- a/test/unit/state/auto-scroll/can-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-scroll.spec.js @@ -1,5 +1,6 @@ // @flow import type { + Area, Position, DroppableDimension, } from '../../../../src/types'; @@ -18,6 +19,7 @@ import { getDroppableDimension, scrollDroppable } from '../../../../src/state/di import setViewport, { resetViewport } from '../../../utils/set-viewport'; import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; +import getMaxScroll from '../../../../src/state/get-max-scroll'; const origin: Position = { x: 0, y: 0 }; const preset = getPreset(); @@ -55,9 +57,9 @@ const scrollable: DroppableDimension = getDroppableDimension({ describe('can scroll', () => { afterEach(() => { - resetViewport(); resetWindowScroll(); resetWindowScrollSize(); + resetViewport(); }); describe('can partially scroll', () => { @@ -527,15 +529,16 @@ describe('can scroll', () => { // tested in get remainder it('should return the overlap', () => { - setViewport(getArea({ + const viewport: Area = getArea({ top: 0, left: 0, right: 100, bottom: 100, - })); + }); + setViewport(viewport); const windowScrollSize = { scrollHeight: 200, - scrollWidth: 100, + scrollWidth: 200, }; setWindowScrollSize(windowScrollSize); const windowScroll: Position = { @@ -543,16 +546,27 @@ describe('can scroll', () => { y: 50, }; setWindowScroll(windowScroll); - const change: Position = { x: 300, y: 300 }; - const space: Position = { - x: windowScrollSize.scrollWidth - windowScroll.x, - y: windowScrollSize.scrollHeight - windowScroll.y, + + // little validation + const maxScroll: Position = getMaxScroll({ + scrollHeight: windowScrollSize.scrollHeight, + scrollWidth: windowScrollSize.scrollWidth, + height: viewport.height, + width: viewport.width, + }); + expect(maxScroll).toEqual({ x: 100, y: 100 }); + + const availableScrollSpace: Position = { + x: 50, + y: 50, }; - const overlap: Position = subtract(change, space); + // cannot be absorbed in the current scroll plane + const bigChange: Position = { x: 300, y: 300 }; + const expectedOverlap: Position = subtract(bigChange, availableScrollSpace); - const result: ?Position = getWindowOverlap(change); + const result: ?Position = getWindowOverlap(bigChange); - expect(result).toEqual(overlap); + expect(result).toEqual(expectedOverlap); }); it('should return null if there is no overlap', () => { @@ -563,7 +577,7 @@ describe('can scroll', () => { bottom: 100, })); const scrollSize = { - scrollHeight: 200, + scrollHeight: 100, scrollWidth: 100, }; setWindowScrollSize(scrollSize); From b535d7c9369b678c5c9eabc7b40298c484ba61b3 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 14 Feb 2018 10:28:50 +1100 Subject: [PATCH 122/163] comment --- src/state/auto-scroller/can-scroll.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state/auto-scroller/can-scroll.js b/src/state/auto-scroller/can-scroll.js index f6c281c165..f2cd191ae7 100644 --- a/src/state/auto-scroller/can-scroll.js +++ b/src/state/auto-scroller/can-scroll.js @@ -19,7 +19,6 @@ type CanScrollArgs = {| const origin: Position = { x: 0, y: 0 }; -// TODO: should this be round or floor? const smallestSigned = apply((value: number) => { if (value === 0) { return 0; From 0476309a344ec0fbceeb495c808bae41e9a8f427 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 14 Feb 2018 16:23:25 +1100 Subject: [PATCH 123/163] initial --- src/state/auto-scroller/can-scroll.js | 2 +- src/state/auto-scroller/fluid-scroller.js | 11 +++-- ...er.js => get-best-scrollable-droppable.js} | 49 ++++++++++++++----- .../state/auto-scroll/fluid-scroller.spec.js | 17 +++++-- test/utils/dimension.js | 30 +++++++++++- 5 files changed, 88 insertions(+), 21 deletions(-) rename src/state/auto-scroller/{get-scrollable-droppable-over.js => get-best-scrollable-droppable.js} (61%) diff --git a/src/state/auto-scroller/can-scroll.js b/src/state/auto-scroller/can-scroll.js index f2cd191ae7..fa80143c35 100644 --- a/src/state/auto-scroller/can-scroll.js +++ b/src/state/auto-scroller/can-scroll.js @@ -1,11 +1,11 @@ // @flow import { add, apply, isEqual } from '../position'; -// TODO: state reaching into VIEW :( import getWindowScroll from '../../window/get-window-scroll'; import getViewport from '../../window/get-viewport'; import getMaxScroll from '../get-max-scroll'; import type { ClosestScrollable, + DraggableDimension, DroppableDimension, Position, Area, diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index e039799de5..f8e8a49a40 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -3,7 +3,7 @@ import rafSchd from 'raf-schd'; import getViewport from '../../window/get-viewport'; import { apply, isEqual } from '../position'; import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; -import getScrollableDroppableOver from './get-scrollable-droppable-over'; +import getBestScrollableDroppable from './get-best-scrollable-droppable'; import { horizontal, vertical } from '../axis'; import { canScrollDroppable, @@ -175,14 +175,15 @@ export default ({ return; } - // 2. We are not scrolling the window. Can we scroll the Droppable? + // 2. We are not scrolling the window. Can we scroll a Droppable? - const droppable: ?DroppableDimension = getScrollableDroppableOver({ - target: center, + const droppable: ?DroppableDimension = getBestScrollableDroppable({ + center, + destination: drag.impact.destination, droppables: state.dimension.droppable, }); - // No scrollable targets + // No scrollable targets if (!droppable) { return; } diff --git a/src/state/auto-scroller/get-scrollable-droppable-over.js b/src/state/auto-scroller/get-best-scrollable-droppable.js similarity index 61% rename from src/state/auto-scroller/get-scrollable-droppable-over.js rename to src/state/auto-scroller/get-best-scrollable-droppable.js index ec60cf1500..344d53068a 100644 --- a/src/state/auto-scroller/get-scrollable-droppable-over.js +++ b/src/state/auto-scroller/get-best-scrollable-droppable.js @@ -2,17 +2,13 @@ import memoizeOne from 'memoize-one'; import isPositionInFrame from '../visibility/is-position-in-frame'; import type { + Position, + DroppableId, DroppableDimension, DroppableDimensionMap, - DroppableId, - Position, + DraggableLocation, } from '../../types'; -type Args = {| - target: Position, - droppables: DroppableDimensionMap, -|}; - const getScrollableDroppables = memoizeOne( (droppables: DroppableDimensionMap): DroppableDimension[] => ( Object.keys(droppables) @@ -33,10 +29,10 @@ const getScrollableDroppables = memoizeOne( ) ); -export default ({ - target, - droppables, -}: Args): ?DroppableDimension => { +const getScrollableDroppableOver = ( + target: Position, + droppables: DroppableDimensionMap +): ?DroppableDimension => { const maybe: ?DroppableDimension = getScrollableDroppables(droppables) .find((droppable: DroppableDimension): boolean => { @@ -48,3 +44,34 @@ export default ({ return maybe; }; + +type Api = {| + center: Position, + destination: ?DraggableLocation, + droppables: DroppableDimensionMap, +|} + +export default ({ + center, + destination, + droppables, +}: Api): ?DroppableDimension => { + // We need to scroll the best droppable frame we can so that the + // placeholder buffer logic works correctly + + if (destination) { + const dimension: DroppableDimension = droppables[destination.droppableId]; + if (!dimension.viewport.closestScrollable) { + return null; + } + return dimension; + } + + // 2. If we are not over a droppable - are we over a droppable frame? + const dimension: ?DroppableDimension = getScrollableDroppableOver( + center, + droppables, + ); + + return dimension; +}; diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index b798b0c4a2..60243af404 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -18,7 +18,7 @@ import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-sc import { vertical, horizontal } from '../../../../src/state/axis'; import createAutoScroller from '../../../../src/state/auto-scroller/'; import * as state from '../../../utils/simple-state-preset'; -import { getPreset } from '../../../utils/dimension'; +import { getInitialImpact, getPreset, withImpact } from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; @@ -111,8 +111,10 @@ describe('fluid auto scrolling', () => { [vertical, horizontal].forEach((axis: Axis) => { describe(`on the ${axis.direction} axis`, () => { const preset = getPreset(axis); - const dragTo = (selection: Position): State => - state.dragging(preset.inHome1.descriptor.id, selection); + const dragTo = ( + selection: Position, + impact?: DragImpact = getInitialImpact(axis, preset.inHome1) + ): State => withImpact(state.dragging(preset.inHome1.descriptor.id, selection), impact); describe('window scrolling', () => { const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); @@ -526,6 +528,11 @@ describe('fluid auto scrolling', () => { (frame[axis.size] - thresholds.maxSpeedAt), frame.center[axis.crossAxisLine], ); + const onEndOfFrame: Position = patch( + axis.line, + frame[axis.size], + frame.center[axis.crossAxisLine], + ); it('should not scroll if not past the start threshold', () => { autoScroller.onStateChange( @@ -980,6 +987,10 @@ describe('fluid auto scrolling', () => { expect(request[axis.crossAxisLine]).toBeLessThan(0); }); }); + + describe('over frame but not a subject', () => { + + }); }); describe('window scrolling before droppable scrolling', () => { diff --git a/test/utils/dimension.js b/test/utils/dimension.js index a7e67bf2bc..b5bb87435a 100644 --- a/test/utils/dimension.js +++ b/test/utils/dimension.js @@ -2,11 +2,14 @@ import getArea from '../../src/state/get-area'; import { patch } from '../../src/state/position'; import { expandByPosition } from '../../src/state/spacing'; +import { noMovement } from '../../src/state/no-impact'; import { getDroppableDimension, getDraggableDimension } from '../../src/state/dimension'; import { vertical } from '../../src/state/axis'; import type { Area, Axis, + DragImpact, + State, Position, Spacing, DroppableDimension, @@ -59,7 +62,32 @@ export const makeScrollable = (droppable: DroppableDimension, amount?: number = shouldClipSubject: true, }, }); -} +}; + +export const getInitialImpact = (axis: Axis, draggable: DraggableDimension) => { + const impact: DragImpact = { + movement: noMovement, + direction: axis.direction, + destination: { + index: draggable.descriptor.index, + droppableId: draggable.descriptor.droppableId, + }, + }; + return impact; +}; + +export const withImpact = (state: State, impact: DragImpact) => { + if (!state.drag) { + throw new Error('invalid state'); + } + return { + ...state, + drag: { + ...state.drag, + impact, + }, + }; +}; export const getPreset = (axis?: Axis = vertical) => { const home: DroppableDimension = getDroppableDimension({ From b41bb48ef5e0ced0450d51a0eed8c382f295f85a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 14 Feb 2018 17:15:23 +1100 Subject: [PATCH 124/163] fixed --- src/state/auto-scroller/can-scroll.js | 1 - src/state/auto-scroller/fluid-scroller.js | 63 ++++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/state/auto-scroller/can-scroll.js b/src/state/auto-scroller/can-scroll.js index fa80143c35..ab0eaeb53d 100644 --- a/src/state/auto-scroller/can-scroll.js +++ b/src/state/auto-scroller/can-scroll.js @@ -5,7 +5,6 @@ import getViewport from '../../window/get-viewport'; import getMaxScroll from '../get-max-scroll'; import type { ClosestScrollable, - DraggableDimension, DroppableDimension, Position, Area, diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index f8e8a49a40..65208b5c1f 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -1,13 +1,15 @@ // @flow import rafSchd from 'raf-schd'; +import memoizeOne from 'memoize-one'; import getViewport from '../../window/get-viewport'; -import { apply, isEqual } from '../position'; +import { add, apply, isEqual } from '../position'; import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; import getBestScrollableDroppable from './get-best-scrollable-droppable'; import { horizontal, vertical } from '../axis'; import { canScrollDroppable, canScrollWindow, + canScrollDroppableWithPlaceholder, } from './can-scroll'; import type { Area, @@ -20,6 +22,7 @@ import type { State, DraggableDimension, ClosestScrollable, + DroppableDimensionViewport, } from '../../types'; // Values used to control how the fluid auto scroll feels @@ -133,6 +136,61 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { return isEqual(required, origin) ? null : required; }; +const withPlaceholder = ( + droppable: DroppableDimension, + draggable: DraggableDimension, +): DroppableDimension => { + const isOverHome: boolean = droppable.descriptor.id === draggable.descriptor.droppableId; + + // only need to add the buffer for foreign lists + if (isOverHome) { + return droppable; + } + + const closest: ?ClosestScrollable = droppable.viewport.closestScrollable; + + // not scrollable + if (!closest) { + return droppable; + } + + const placeholder: Position = { + x: draggable.placeholder.withoutMargin.width, + y: draggable.placeholder.withoutMargin.height, + }; + + const max: Position = add(closest.scroll.max, placeholder); + const current: Position = { + x: Math.min(closest.scroll.current.x, max.x), + y: Math.min(closest.scroll.current.y, max.y), + }; + + const withBuffer: ClosestScrollable = { + frame: closest.frame, + shouldClipSubject: closest.shouldClipSubject, + scroll: { + initial: closest.scroll.initial, + current, + max, + diff: closest.scroll.diff, + }, + }; + + const viewport: DroppableDimensionViewport = { + closestScrollable: withBuffer, + subject: droppable.viewport.subject, + clipped: droppable.viewport.clipped, + }; + + // $ExpectError - using spread + const modified: DroppableDimension = { + ...droppable, + viewport, + }; + + return modified; +}; + type Api = {| scrollWindow: (offset: Position) => void, scrollDroppable: (id: DroppableId, offset: Position) => void, @@ -196,8 +254,9 @@ export default ({ } const requiredFrameScroll: ?Position = getRequiredScroll(closestScrollable.frame, center); + const extended: DroppableDimension = withPlaceholder(droppable, draggable); - if (requiredFrameScroll && canScrollDroppable(droppable, requiredFrameScroll)) { + if (requiredFrameScroll && canScrollDroppable(extended, requiredFrameScroll)) { scheduleDroppableScroll(droppable.descriptor.id, requiredFrameScroll); } }; From 61ceaee45e2d2903cf1060ac496acf13cfef6d99 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 15 Feb 2018 11:08:03 +1100 Subject: [PATCH 125/163] updating fluid scoller tests. making state preset axis aware --- src/state/auto-scroller/fluid-scroller.js | 105 +++---- src/state/dimension.js | 6 +- .../move-to-new-droppable/to-home-list.js | 2 +- .../state/auto-scroll/fluid-scroller.spec.js | 202 +++++++++----- test/unit/state/dimension.spec.js | 58 +++- test/utils/dimension.js | 32 ++- test/utils/get-simple-state-preset.js | 263 ++++++++++++++++++ test/utils/simple-state-preset.js | 249 ----------------- 8 files changed, 534 insertions(+), 383 deletions(-) create mode 100644 test/utils/get-simple-state-preset.js delete mode 100644 test/utils/simple-state-preset.js diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index 65208b5c1f..8a0b7a240e 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -1,15 +1,13 @@ // @flow import rafSchd from 'raf-schd'; -import memoizeOne from 'memoize-one'; import getViewport from '../../window/get-viewport'; -import { add, apply, isEqual } from '../position'; +import { add, apply, isEqual, patch } from '../position'; import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; import getBestScrollableDroppable from './get-best-scrollable-droppable'; import { horizontal, vertical } from '../axis'; import { - canScrollDroppable, canScrollWindow, - canScrollDroppableWithPlaceholder, + canPartiallyScroll, } from './can-scroll'; import type { Area, @@ -22,7 +20,6 @@ import type { State, DraggableDimension, ClosestScrollable, - DroppableDimensionViewport, } from '../../types'; // Values used to control how the fluid auto scroll feels @@ -136,59 +133,49 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { return isEqual(required, origin) ? null : required; }; +type WithPlaceholderResult = {| + current: Position, + max: Position, +|} + const withPlaceholder = ( droppable: DroppableDimension, draggable: DraggableDimension, -): DroppableDimension => { - const isOverHome: boolean = droppable.descriptor.id === draggable.descriptor.droppableId; - - // only need to add the buffer for foreign lists - if (isOverHome) { - return droppable; - } - +): ?WithPlaceholderResult => { const closest: ?ClosestScrollable = droppable.viewport.closestScrollable; - // not scrollable if (!closest) { - return droppable; + return null; } - const placeholder: Position = { - x: draggable.placeholder.withoutMargin.width, - y: draggable.placeholder.withoutMargin.height, - }; - - const max: Position = add(closest.scroll.max, placeholder); - const current: Position = { - x: Math.min(closest.scroll.current.x, max.x), - y: Math.min(closest.scroll.current.y, max.y), - }; + const isOverHome: boolean = droppable.descriptor.id === draggable.descriptor.droppableId; + const max: Position = closest.scroll.max; + const current: Position = closest.scroll.current; - const withBuffer: ClosestScrollable = { - frame: closest.frame, - shouldClipSubject: closest.shouldClipSubject, - scroll: { - initial: closest.scroll.initial, - current, - max, - diff: closest.scroll.diff, - }, - }; + // only need to add the buffer for foreign lists + if (isOverHome) { + return { max, current }; + } - const viewport: DroppableDimensionViewport = { - closestScrollable: withBuffer, - subject: droppable.viewport.subject, - clipped: droppable.viewport.clipped, + const spaceForPlaceholder: Position = patch( + droppable.axis.line, + draggable.placeholder.withoutMargin[droppable.axis.size] + ); + + const newMax: Position = add(max, spaceForPlaceholder); + // because we are pulling the max forward, on subsequent updates + // it is possible for the current position to be greater than the max + // as such we need to ensure that the current position is never bigger + // than the max position + const newCurrent: Position = { + x: Math.min(current.x, newMax.x), + y: Math.min(current.y, newMax.y), }; - // $ExpectError - using spread - const modified: DroppableDimension = { - ...droppable, - viewport, + return { + max: newMax, + current: newCurrent, }; - - return modified; }; type Api = {| @@ -208,7 +195,7 @@ export default ({ const scheduleWindowScroll = rafSchd(scrollWindow); const scheduleDroppableScroll = rafSchd(scrollDroppable); - const result = (state: State): void => { + const scroller = (state: State): void => { const drag: ?DragState = state.drag; if (!drag) { console.error('Invalid drag state'); @@ -254,18 +241,36 @@ export default ({ } const requiredFrameScroll: ?Position = getRequiredScroll(closestScrollable.frame, center); - const extended: DroppableDimension = withPlaceholder(droppable, draggable); - if (requiredFrameScroll && canScrollDroppable(extended, requiredFrameScroll)) { + if (!requiredFrameScroll) { + return; + } + + // need to adjust the current and max scroll positions to account for placeholders + const result: ?WithPlaceholderResult = withPlaceholder(droppable, draggable); + + if (!result) { + return; + } + + // using the can partially scroll function directly as we want to control + // the current and max values without modifying the droppable + const canScrollDroppable: boolean = canPartiallyScroll({ + max: result.max, + current: result.current, + change: requiredFrameScroll, + }); + + if (canScrollDroppable) { scheduleDroppableScroll(droppable.descriptor.id, requiredFrameScroll); } }; - result.cancel = () => { + scroller.cancel = () => { scheduleWindowScroll.cancel(); scheduleDroppableScroll.cancel(); }; - return result; + return scroller; }; diff --git a/src/state/dimension.js b/src/state/dimension.js index 7c16834977..dbc9991749 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -2,7 +2,7 @@ import { vertical, horizontal } from './axis'; import getArea from './get-area'; import { offsetByPosition, expandBySpacing } from './spacing'; -import { subtract, negate } from './position'; +import { subtract, negate, isEqual } from './position'; import getMaxScroll from './get-max-scroll'; import type { DraggableDescriptor, @@ -99,6 +99,9 @@ export const scrollDroppable = ( // (scrolling down pulls an item upwards) const scrollDisplacement: Position = negate(scrollDiff); + // Sometimes it is possible to scroll beyond the max point. + // This can occur when scrolling a foreign list that now has a placeholder. + const closestScrollable: ClosestScrollable = { frame: existingScrollable.frame, shouldClipSubject: existingScrollable.shouldClipSubject, @@ -109,6 +112,7 @@ export const scrollDroppable = ( value: scrollDiff, displacement: scrollDisplacement, }, + // TODO: rename 'softMax?' max: existingScrollable.scroll.max, }, }; diff --git a/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js index 36ecf0b68a..483c38975f 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js +++ b/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js @@ -65,7 +65,7 @@ export default ({ }; return { - pageCenter: newCenter, + pageCenter: withDroppableDisplacement(droppable, newCenter), impact: newImpact, }; } diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index 60243af404..6535eb7a9e 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -6,6 +6,7 @@ import type { State, DraggableDimension, DroppableDimension, + DragImpact, } from '../../../../src/types'; import type { AutoScroller } from '../../../../src/state/auto-scroller/auto-scroller-types'; import type { PixelThresholds } from '../../../../src/state/auto-scroller/fluid-scroller'; @@ -15,66 +16,21 @@ import getArea from '../../../../src/state/get-area'; import setViewport, { resetViewport } from '../../../utils/set-viewport'; import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; +import { noMovement } from '../../../../src/state/no-impact'; import { vertical, horizontal } from '../../../../src/state/axis'; import createAutoScroller from '../../../../src/state/auto-scroller/'; -import * as state from '../../../utils/simple-state-preset'; -import { getInitialImpact, getPreset, withImpact } from '../../../utils/dimension'; +import getStatePreset from '../../../utils/get-simple-state-preset'; +import { + getInitialImpact, + getClosestScrollable, + getPreset, + withImpact, + addDraggable, + addDroppable, +} from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; -const addDroppable = (base: State, droppable: DroppableDimension): State => ({ - ...base, - dimension: { - ...base.dimension, - droppable: { - ...base.dimension.droppable, - [droppable.descriptor.id]: droppable, - }, - }, -}); - -const addDraggable = (base: State, draggable: DraggableDimension): State => ({ - ...base, - dimension: { - ...base.dimension, - draggable: { - ...base.dimension.draggable, - [draggable.descriptor.id]: draggable, - }, - }, -}); - -const scrollableScrollSize = { - scrollWidth: 800, - scrollHeight: 800, -}; -const frame: Area = getArea({ - top: 0, - left: 0, - right: 600, - bottom: 600, -}); -const scrollable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'drop-1', - type: 'TYPE', - }, - client: getArea({ - top: 0, - left: 0, - // bigger than the frame - right: scrollableScrollSize.scrollWidth, - bottom: scrollableScrollSize.scrollHeight, - }), - closest: { - frameClient: frame, - scrollWidth: scrollableScrollSize.scrollWidth, - scrollHeight: scrollableScrollSize.scrollHeight, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, -}); - const windowScrollSize = { scrollHeight: 2000, scrollWidth: 1600, @@ -108,13 +64,49 @@ describe('fluid auto scrolling', () => { requestAnimationFrame.reset(); }); - [vertical, horizontal].forEach((axis: Axis) => { + [horizontal].forEach((axis: Axis) => { describe(`on the ${axis.direction} axis`, () => { const preset = getPreset(axis); + const state = getStatePreset(axis); + const scrollableScrollSize = { + scrollWidth: 800, + scrollHeight: 800, + }; + const frame: Area = getArea({ + top: 0, + left: 0, + right: 600, + bottom: 600, + }); + + const scrollable: DroppableDimension = getDroppableDimension({ + // stealing the home descriptor + descriptor: preset.home.descriptor, + direction: axis.direction, + client: getArea({ + top: 0, + left: 0, + // bigger than the frame + right: scrollableScrollSize.scrollWidth, + bottom: scrollableScrollSize.scrollHeight, + }), + closest: { + frameClient: frame, + scrollWidth: scrollableScrollSize.scrollWidth, + scrollHeight: scrollableScrollSize.scrollHeight, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const dragTo = ( selection: Position, - impact?: DragImpact = getInitialImpact(axis, preset.inHome1) - ): State => withImpact(state.dragging(preset.inHome1.descriptor.id, selection), impact); + // seeding that we are over the home droppable + impact?: DragImpact = getInitialImpact(axis, preset.inHome1), + ): State => withImpact( + state.dragging(preset.inHome1.descriptor.id, selection), + impact, + ); describe('window scrolling', () => { const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); @@ -688,23 +680,103 @@ describe('fluid auto scrolling', () => { expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); - it('should not scroll if the droppable is unable to be scrolled', () => { - const target: Position = onMaxBoundary; - if (!scrollable.viewport.closestScrollable) { - throw new Error('Invalid test setup'); - } + it('should allow scrolling to the end of the droppable', () => { + const target: Position = onEndOfFrame; // scrolling to max scroll point - const maxChange: Position = scrollable.viewport.closestScrollable.scroll.max; + const maxChange: Position = getClosestScrollable(scrollable).scroll.max; const scrolled: DroppableDimension = scrollDroppable(scrollable, maxChange); autoScroller.onStateChange( state.idle, - addDroppable(dragTo(target), scrolled) + addDroppable(dragTo(target), scrolled), ); requestAnimationFrame.flush(); expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); + + describe('over home list', () => { + it('should not scroll if the droppable if moving past the end of the frame', () => { + const target: Position = add(onEndOfFrame, patch(axis.line, 1)); + // scrolling to max scroll point + const maxChange: Position = getClosestScrollable(scrollable).scroll.max; + const scrolled: DroppableDimension = scrollDroppable(scrollable, maxChange); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target), scrolled), + ); + requestAnimationFrame.flush(); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); + + describe('over foreign list', () => { + // $ExpectError - using spread + const foreign: DroppableDimension = { + ...scrollable, + descriptor: preset.foreign.descriptor, + }; + const placeholder: Position = patch( + axis.line, + preset.inHome1.placeholder.withoutMargin[axis.size], + ); + const overForeign: DragImpact = { + movement: noMovement, + direction: foreign.axis.direction, + destination: { + index: 0, + droppableId: foreign.descriptor.id, + }, + }; + + it('should allow scrolling up to the end of the frame + the size of the placeholder', () => { + // scrolling to just before the end of the placeholder + // this goes beyond the usual max scroll. + const scroll: Position = add( + // usual max scroll + getClosestScrollable(foreign).scroll.max, + // with a small bit of room towards the end of the placeholder space + subtract(placeholder, patch(axis.line, 1)) + ); + const scrolledForeign: DroppableDimension = scrollDroppable(foreign, scroll); + const target: Position = add(onEndOfFrame, placeholder); + const expected: Position = patch(axis.line, config.maxScrollSpeed); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target, overForeign), scrolledForeign), + ); + requestAnimationFrame.step(); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith(foreign.descriptor.id, expected); + }); + + it('should not allow scrolling past the placeholder buffer', () => { + // already on the placeholder + const scroll: Position = add( + // usual max scroll + getClosestScrollable(foreign).scroll.max, + // with the placeholder + placeholder, + ); + const scrolledForeign: DroppableDimension = scrollDroppable(foreign, scroll); + // targeting beyond the placeholder + const target: Position = add( + add(onEndOfFrame, placeholder), + patch(axis.line, 1), + ); + + autoScroller.onStateChange( + state.idle, + addDroppable(dragTo(target, overForeign), scrolledForeign), + ); + requestAnimationFrame.flush(); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); }); describe('moving backward to the start of droppable', () => { diff --git a/test/unit/state/dimension.spec.js b/test/unit/state/dimension.spec.js index 32a1bc2839..76f5120656 100644 --- a/test/unit/state/dimension.spec.js +++ b/test/unit/state/dimension.spec.js @@ -10,6 +10,7 @@ import { offsetByPosition } from '../../../src/state/spacing'; import getArea from '../../../src/state/get-area'; import { negate } from '../../../src/state/position'; import getMaxScroll from '../../../src/state/get-max-scroll'; +import { getClosestScrollable } from '../../utils/dimension'; import type { Area, Spacing, @@ -49,13 +50,6 @@ const windowScroll: Position = { }; const origin: Position = { x: 0, y: 0 }; -const getClosestScrollable = (droppable: DroppableDimension): ClosestScrollable => { - if (!droppable.viewport.closestScrollable) { - throw new Error('Cannot get closest scrollable'); - } - return droppable.viewport.closestScrollable; -}; - describe('dimension', () => { describe('draggable dimension', () => { const dimension: DraggableDimension = getDraggableDimension({ @@ -379,19 +373,23 @@ describe('dimension', () => { describe('scrolling a droppable', () => { it('should update the frame scroll and the clipping', () => { + const scrollSize = { + scrollHeight: 500, + scrollWidth: 100, + }; const subject = getArea({ // 500 px high top: 0, - bottom: 500, - right: 100, + bottom: scrollSize.scrollHeight, + right: scrollSize.scrollWidth, left: 0, }); const frameClient = getArea({ // only viewing top 100px - top: 0, bottom: 100, // unchanged - right: 100, + top: 0, + right: scrollSize.scrollWidth, left: 0, }); const frameScroll: Position = { x: 0, y: 0 }; @@ -401,8 +399,8 @@ describe('dimension', () => { closest: { frameClient, scroll: frameScroll, - scrollWidth: 500, - scrollHeight: 100, + scrollWidth: scrollSize.scrollWidth, + scrollHeight: scrollSize.scrollHeight, shouldClipSubject: true, }, }); @@ -431,8 +429,8 @@ describe('dimension', () => { displacement: negate(newScroll), }, max: getMaxScroll({ - scrollHeight: 100, - scrollWidth: 500, + scrollWidth: scrollSize.scrollWidth, + scrollHeight: scrollSize.scrollHeight, width: frameClient.width, height: frameClient.height, }), @@ -448,6 +446,36 @@ describe('dimension', () => { left: 0, })); }); + + it('should increase the max scroll position if the requested scroll is bigger than the max', () => { + const scrollable: DroppableDimension = getDroppableDimension({ + descriptor: droppableDescriptor, + client: getArea({ + top: 0, + left: 0, + right: 200, + bottom: 200, + }), + closest: { + frameClient: getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + scroll: { x: 0, y: 0 }, + scrollWidth: 200, + scrollHeight: 200, + shouldClipSubject: true, + }, + }); + + const scrolled: DroppableDimension = scrollDroppable(scrollable, { x: 300, y: 300 }); + + expect(getClosestScrollable(scrolled).scroll.max).toEqual({ x: 300, y: 300 }); + // original max + expect(getClosestScrollable(scrollable).scroll.max).toEqual({ x: 100, y: 100 }); + }); }); describe('subject clipping', () => { diff --git a/test/utils/dimension.js b/test/utils/dimension.js index b5bb87435a..e29c7c9ba6 100644 --- a/test/utils/dimension.js +++ b/test/utils/dimension.js @@ -1,7 +1,5 @@ // @flow import getArea from '../../src/state/get-area'; -import { patch } from '../../src/state/position'; -import { expandByPosition } from '../../src/state/spacing'; import { noMovement } from '../../src/state/no-impact'; import { getDroppableDimension, getDraggableDimension } from '../../src/state/dimension'; import { vertical } from '../../src/state/axis'; @@ -11,6 +9,7 @@ import type { DragImpact, State, Position, + ClosestScrollable, Spacing, DroppableDimension, DraggableDimension, @@ -89,6 +88,35 @@ export const withImpact = (state: State, impact: DragImpact) => { }; }; +export const addDroppable = (base: State, droppable: DroppableDimension): State => ({ + ...base, + dimension: { + ...base.dimension, + droppable: { + ...base.dimension.droppable, + [droppable.descriptor.id]: droppable, + }, + }, +}); + +export const addDraggable = (base: State, draggable: DraggableDimension): State => ({ + ...base, + dimension: { + ...base.dimension, + draggable: { + ...base.dimension.draggable, + [draggable.descriptor.id]: draggable, + }, + }, +}); + +export const getClosestScrollable = (droppable: DroppableDimension): ClosestScrollable => { + if (!droppable.viewport.closestScrollable) { + throw new Error('Cannot get closest scrollable'); + } + return droppable.viewport.closestScrollable; +}; + export const getPreset = (axis?: Axis = vertical) => { const home: DroppableDimension = getDroppableDimension({ descriptor: { diff --git a/test/utils/get-simple-state-preset.js b/test/utils/get-simple-state-preset.js new file mode 100644 index 0000000000..1abd5f3d58 --- /dev/null +++ b/test/utils/get-simple-state-preset.js @@ -0,0 +1,263 @@ +// @flow +import { getPreset } from './dimension'; +import noImpact from '../../src/state/no-impact'; +import type { + State, + DraggableDescriptor, + DroppableDescriptor, + DimensionState, + DraggableDimension, + DroppableDimension, + CurrentDragPositions, + InitialDragPositions, + Position, + DragState, + DropResult, + PendingDrop, + DropReason, + DraggableId, + DragImpact, +} from '../../src/types'; + +export default (axis: Axis) => { + const preset = getPreset(axis); + + const getDimensionState = (request: DraggableId): DimensionState => { + const draggable: DraggableDimension = preset.draggables[request]; + const home: DroppableDimension = preset.droppables[draggable.descriptor.droppableId]; + + const result: DimensionState = { + request, + draggable: { [draggable.descriptor.id]: draggable }, + droppable: { [home.descriptor.id]: home }, + }; + return result; + }; + + const idle: State = { + phase: 'IDLE', + drag: null, + drop: null, + dimension: { + request: null, + draggable: {}, + droppable: {}, + }, + }; + + const preparing: State = { + ...idle, + phase: 'PREPARING', + }; + + const requesting = (request?: DraggableId = preset.inHome1.descriptor.id): State => { + const result: State = { + phase: 'COLLECTING_INITIAL_DIMENSIONS', + drag: null, + drop: null, + dimension: { + request, + draggable: {}, + droppable: {}, + }, + }; + return result; + }; + + const origin: Position = { x: 0, y: 0 }; + + const dragging = ( + id?: DraggableId = preset.inHome1.descriptor.id, + selection?: Position, + ): State => { + // will populate the dimension state with the initial dimensions + const draggable: DraggableDimension = preset.draggables[id]; + // either use the provided selection or use the draggable's center + const clientSelection: Position = selection || draggable.client.withMargin.center; + const initialPosition: InitialDragPositions = { + selection: clientSelection, + center: clientSelection, + }; + const clientPositions: CurrentDragPositions = { + selection: clientSelection, + center: clientSelection, + offset: origin, + }; + + const drag: DragState = { + initial: { + descriptor: draggable.descriptor, + autoScrollMode: 'FLUID', + client: initialPosition, + page: initialPosition, + windowScroll: origin, + }, + current: { + client: clientPositions, + page: clientPositions, + windowScroll: origin, + shouldAnimate: false, + }, + impact: noImpact, + scrollJumpRequest: null, + }; + + const result: State = { + phase: 'DRAGGING', + drag, + drop: null, + dimension: getDimensionState(id), + }; + + return result; + }; + + const scrollJumpRequest = (request: Position): State => { + const id: DraggableId = preset.inHome1.descriptor.id; + // will populate the dimension state with the initial dimensions + const draggable: DraggableDimension = preset.draggables[id]; + // either use the provided selection or use the draggable's center + const initialPosition: InitialDragPositions = { + selection: draggable.client.withMargin.center, + center: draggable.client.withMargin.center, + }; + const clientPositions: CurrentDragPositions = { + selection: draggable.client.withMargin.center, + center: draggable.client.withMargin.center, + offset: origin, + }; + + const impact: DragImpact = { + movement: { + displaced: [], + amount: origin, + isBeyondStartPosition: false, + }, + direction: preset.home.axis.direction, + destination: { + index: preset.inHome1.descriptor.index, + droppableId: preset.inHome1.descriptor.droppableId, + }, + }; + + const drag: DragState = { + initial: { + descriptor: draggable.descriptor, + autoScrollMode: 'JUMP', + client: initialPosition, + page: initialPosition, + windowScroll: origin, + }, + current: { + client: clientPositions, + page: clientPositions, + windowScroll: origin, + shouldAnimate: true, + }, + impact, + scrollJumpRequest: request, + }; + + const result: State = { + phase: 'DRAGGING', + drag, + drop: null, + dimension: getDimensionState(id), + }; + + return result; + }; + + const getDropAnimating = (id: DraggableId, reason: DropReason): State => { + const descriptor: DraggableDescriptor = preset.draggables[id].descriptor; + const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor; + const pending: PendingDrop = { + newHomeOffset: origin, + impact: noImpact, + result: { + draggableId: descriptor.id, + type: home.type, + source: { + droppableId: home.id, + index: descriptor.index, + }, + destination: null, + reason, + }, + }; + + const result: State = { + phase: 'DROP_ANIMATING', + drag: null, + drop: { + pending, + result: null, + }, + dimension: getDimensionState(descriptor.id), + }; + return result; + }; + + const dropAnimating = ( + id?: DraggableId = preset.inHome1.descriptor.id + ): State => getDropAnimating(id, 'DROP'); + + const userCancel = ( + id?: DraggableId = preset.inHome1.descriptor.id + ): State => getDropAnimating(id, 'CANCEL'); + + const dropComplete = ( + id?: DraggableId = preset.inHome1.descriptor.id + ): State => { + const descriptor: DraggableDescriptor = preset.draggables[id].descriptor; + const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor; + const result: DropResult = { + draggableId: descriptor.id, + type: home.type, + source: { + droppableId: home.id, + index: descriptor.index, + }, + destination: null, + reason: 'DROP', + }; + + const value: State = { + phase: 'DROP_COMPLETE', + drag: null, + drop: { + pending: null, + result, + }, + dimension: { + request: null, + draggable: {}, + droppable: {}, + }, + }; + return value; + }; + + const allPhases = (id? : DraggableId = preset.inHome1.descriptor.id): State[] => [ + idle, + preparing, + requesting(id), + dragging(id), + dropAnimating(id), + userCancel(id), + dropComplete(id), + ]; + + return { + idle, + preparing, + requesting, + dragging, + scrollJumpRequest, + dropAnimating, + userCancel, + dropComplete, + allPhases, + }; +}; + diff --git a/test/utils/simple-state-preset.js b/test/utils/simple-state-preset.js deleted file mode 100644 index 5b490b2e59..0000000000 --- a/test/utils/simple-state-preset.js +++ /dev/null @@ -1,249 +0,0 @@ -// @flow -import { getPreset } from './dimension'; -import noImpact from '../../src/state/no-impact'; -import type { - State, - DraggableDescriptor, - DroppableDescriptor, - DimensionState, - DraggableDimension, - DroppableDimension, - CurrentDragPositions, - InitialDragPositions, - Position, - DragState, - DropResult, - PendingDrop, - DropReason, - DraggableId, - DragImpact, -} from '../../src/types'; - -const preset = getPreset(); - -const getDimensionState = (request: DraggableId): DimensionState => { - const draggable: DraggableDimension = preset.draggables[request]; - const home: DroppableDimension = preset.droppables[draggable.descriptor.droppableId]; - - const result: DimensionState = { - request, - draggable: { [draggable.descriptor.id]: draggable }, - droppable: { [home.descriptor.id]: home }, - }; - return result; -}; - -export const idle: State = { - phase: 'IDLE', - drag: null, - drop: null, - dimension: { - request: null, - draggable: {}, - droppable: {}, - }, -}; - -export const preparing: State = { - ...idle, - phase: 'PREPARING', -}; - -export const requesting = (request?: DraggableId = preset.inHome1.descriptor.id): State => { - const result: State = { - phase: 'COLLECTING_INITIAL_DIMENSIONS', - drag: null, - drop: null, - dimension: { - request, - draggable: {}, - droppable: {}, - }, - }; - return result; -}; - -const origin: Position = { x: 0, y: 0 }; - -export const dragging = ( - id?: DraggableId = preset.inHome1.descriptor.id, - selection?: Position, -): State => { - // will populate the dimension state with the initial dimensions - const draggable: DraggableDimension = preset.draggables[id]; - // either use the provided selection or use the draggable's center - const clientSelection: Position = selection || draggable.client.withMargin.center; - const initialPosition: InitialDragPositions = { - selection: clientSelection, - center: clientSelection, - }; - const clientPositions: CurrentDragPositions = { - selection: clientSelection, - center: clientSelection, - offset: origin, - }; - - const drag: DragState = { - initial: { - descriptor: draggable.descriptor, - autoScrollMode: 'FLUID', - client: initialPosition, - page: initialPosition, - windowScroll: origin, - }, - current: { - client: clientPositions, - page: clientPositions, - windowScroll: origin, - shouldAnimate: false, - }, - impact: noImpact, - scrollJumpRequest: null, - }; - - const result: State = { - phase: 'DRAGGING', - drag, - drop: null, - dimension: getDimensionState(id), - }; - - return result; -}; - -export const scrollJumpRequest = (request: Position): State => { - const id: DraggableId = preset.inHome1.descriptor.id; - // will populate the dimension state with the initial dimensions - const draggable: DraggableDimension = preset.draggables[id]; - // either use the provided selection or use the draggable's center - const initialPosition: InitialDragPositions = { - selection: draggable.client.withMargin.center, - center: draggable.client.withMargin.center, - }; - const clientPositions: CurrentDragPositions = { - selection: draggable.client.withMargin.center, - center: draggable.client.withMargin.center, - offset: origin, - }; - - const impact: DragImpact = { - movement: { - displaced: [], - amount: origin, - isBeyondStartPosition: false, - }, - direction: preset.home.axis.direction, - destination: { - index: preset.inHome1.descriptor.index, - droppableId: preset.inHome1.descriptor.droppableId, - }, - }; - - const drag: DragState = { - initial: { - descriptor: draggable.descriptor, - autoScrollMode: 'JUMP', - client: initialPosition, - page: initialPosition, - windowScroll: origin, - }, - current: { - client: clientPositions, - page: clientPositions, - windowScroll: origin, - shouldAnimate: true, - }, - impact, - scrollJumpRequest: request, - }; - - const result: State = { - phase: 'DRAGGING', - drag, - drop: null, - dimension: getDimensionState(id), - }; - - return result; -}; - -const getDropAnimating = (id: DraggableId, reason: DropReason): State => { - const descriptor: DraggableDescriptor = preset.draggables[id].descriptor; - const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor; - const pending: PendingDrop = { - newHomeOffset: origin, - impact: noImpact, - result: { - draggableId: descriptor.id, - type: home.type, - source: { - droppableId: home.id, - index: descriptor.index, - }, - destination: null, - reason, - }, - }; - - const result: State = { - phase: 'DROP_ANIMATING', - drag: null, - drop: { - pending, - result: null, - }, - dimension: getDimensionState(descriptor.id), - }; - return result; -}; - -export const dropAnimating = ( - id?: DraggableId = preset.inHome1.descriptor.id -): State => getDropAnimating(id, 'DROP'); - -export const userCancel = ( - id?: DraggableId = preset.inHome1.descriptor.id -): State => getDropAnimating(id, 'CANCEL'); - -export const dropComplete = ( - id?: DraggableId = preset.inHome1.descriptor.id -): State => { - const descriptor: DraggableDescriptor = preset.draggables[id].descriptor; - const home: DroppableDescriptor = preset.droppables[descriptor.droppableId].descriptor; - const result: DropResult = { - draggableId: descriptor.id, - type: home.type, - source: { - droppableId: home.id, - index: descriptor.index, - }, - destination: null, - reason: 'DROP', - }; - - const value: State = { - phase: 'DROP_COMPLETE', - drag: null, - drop: { - pending: null, - result, - }, - dimension: { - request: null, - draggable: {}, - droppable: {}, - }, - }; - return value; -}; - -export const allPhases = (id? : DraggableId = preset.inHome1.descriptor.id): State[] => [ - idle, - preparing, - requesting(id), - dragging(id), - dropAnimating(id), - userCancel(id), - dropComplete(id), -]; - From 77948a1dff9eb9fd4ef0b4faaef96b1697bfb808 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 15 Feb 2018 11:13:01 +1100 Subject: [PATCH 126/163] state preset is now axis aware --- test/unit/state/action-creators.spec.js | 4 +++- test/unit/state/auto-scroll/jump-scroller.spec.js | 8 ++++++-- test/unit/state/can-start-drag.spec.js | 3 ++- test/unit/state/hook-caller.spec.js | 3 ++- test/unit/view/connected-draggable.spec.js | 3 ++- test/unit/view/connected-droppable.spec.js | 3 ++- test/unit/view/dimension-marshal.spec.js | 3 ++- test/unit/view/style-marshal.spec.js | 4 +++- test/utils/get-simple-state-preset.js | 4 +++- 9 files changed, 25 insertions(+), 10 deletions(-) diff --git a/test/unit/state/action-creators.spec.js b/test/unit/state/action-creators.spec.js index 24966a9efd..630b55b9b0 100644 --- a/test/unit/state/action-creators.spec.js +++ b/test/unit/state/action-creators.spec.js @@ -12,7 +12,7 @@ import { } from '../../../src/state/action-creators'; import createStore from '../../../src/state/create-store'; import { getPreset } from '../../utils/dimension'; -import * as state from '../../utils/simple-state-preset'; +import getStatePreset from '../../utils/get-simple-state-preset'; import type { State, Position, @@ -42,6 +42,8 @@ const liftDefaults: LiftFnArgs = { isScrollAllowed: true, }; +const state = getStatePreset(); + const liftWithDefaults = (args?: LiftFnArgs = liftDefaults) => { const { id, client, windowScroll, isScrollAllowed } = args; return lift(id, client, windowScroll, isScrollAllowed); diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index cfe9741acd..48362e2751 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -15,7 +15,7 @@ import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-w import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; import { vertical, horizontal } from '../../../../src/state/axis'; import createAutoScroller from '../../../../src/state/auto-scroller'; -import * as state from '../../../utils/simple-state-preset'; +import getStatePreset from '../../../utils/get-simple-state-preset'; import { getPreset } from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; @@ -87,6 +87,7 @@ describe('jump auto scrolling', () => { [vertical, horizontal].forEach((axis: Axis) => { describe(`on the ${axis.direction} axis`, () => { const preset = getPreset(axis); + const state = getStatePreset(axis); describe('window scrolling', () => { it('should not scroll if the item is bigger than the viewport', () => { @@ -459,7 +460,10 @@ describe('jump auto scrolling', () => { setWindowScroll(windowScroll); // Setting the droppable scroll so it has a small amount of available space const availableDroppableScroll: Position = patch(axis.line, 1); - const droppableScroll: Position = subtract(maxDroppableScroll, availableDroppableScroll); + const droppableScroll: Position = subtract( + maxDroppableScroll, + availableDroppableScroll + ); const scrolled: DroppableDimension = scrollDroppable( scrollable, droppableScroll, diff --git a/test/unit/state/can-start-drag.spec.js b/test/unit/state/can-start-drag.spec.js index a4de7a0157..8718e110ca 100644 --- a/test/unit/state/can-start-drag.spec.js +++ b/test/unit/state/can-start-drag.spec.js @@ -1,10 +1,11 @@ // @flow import canStartDrag from '../../../src/state/can-start-drag'; -import * as state from '../../utils/simple-state-preset'; +import getStatePreset from '../../utils/get-simple-state-preset'; import { getPreset } from '../../utils/dimension'; import type { State } from '../../../src/types'; const preset = getPreset(); +const state = getStatePreset(); describe('can start drag', () => { describe('at rest', () => { diff --git a/test/unit/state/hook-caller.spec.js b/test/unit/state/hook-caller.spec.js index 176cb2e7f0..ac83239f50 100644 --- a/test/unit/state/hook-caller.spec.js +++ b/test/unit/state/hook-caller.spec.js @@ -2,7 +2,7 @@ import createHookCaller from '../../../src/state/hooks/hook-caller'; import messagePreset from '../../../src/state/hooks/message-preset'; import type { HookCaller } from '../../../src/state/hooks/hooks-types'; -import * as state from '../../utils/simple-state-preset'; +import getStatePreset from '../../utils/get-simple-state-preset'; import { getPreset } from '../../utils/dimension'; import noImpact, { noMovement } from '../../../src/state/no-impact'; import type { @@ -21,6 +21,7 @@ import type { } from '../../../src/types'; const preset = getPreset(); +const state = getStatePreset(); const noDimensions: DimensionState = { request: null, diff --git a/test/unit/view/connected-draggable.spec.js b/test/unit/view/connected-draggable.spec.js index b5b7f6280d..00ed0fb995 100644 --- a/test/unit/view/connected-draggable.spec.js +++ b/test/unit/view/connected-draggable.spec.js @@ -6,7 +6,7 @@ import Draggable, { makeSelector } from '../../../src/view/draggable/connected-d import { getPreset } from '../../utils/dimension'; import { negate } from '../../../src/state/position'; import createDimensionMarshal from '../../../src/state/dimension-marshal/dimension-marshal'; -import * as state from '../../utils/simple-state-preset'; +import getStatePreset from '../../utils/get-simple-state-preset'; import { combine, withStore, @@ -32,6 +32,7 @@ import type { } from '../../../src/types'; const preset = getPreset(); +const state = getStatePreset(); const move = (previous: State, offset: Position): State => { const clientPositions: CurrentDragPositions = { offset, diff --git a/test/unit/view/connected-droppable.spec.js b/test/unit/view/connected-droppable.spec.js index 235fc9ff4e..8652b78660 100644 --- a/test/unit/view/connected-droppable.spec.js +++ b/test/unit/view/connected-droppable.spec.js @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import { mount } from 'enzyme'; import { withStore, combine, withDimensionMarshal } from '../../utils/get-context-options'; import Droppable, { makeSelector } from '../../../src/view/droppable/connected-droppable'; -import * as state from '../../utils/simple-state-preset'; +import getStatePreset from '../../utils/get-simple-state-preset'; import forceUpdate from '../../utils/force-update'; import { getPreset } from '../../utils/dimension'; import type { @@ -20,6 +20,7 @@ import type { } from '../../../src/types'; const preset = getPreset(); +const state = getStatePreset(); const clone = (value: State): State => (JSON.parse(JSON.stringify(value)) : any); const getOwnProps = (dimension: DroppableDimension): OwnProps => ({ diff --git a/test/unit/view/dimension-marshal.spec.js b/test/unit/view/dimension-marshal.spec.js index 5f3497cf17..866a27d222 100644 --- a/test/unit/view/dimension-marshal.spec.js +++ b/test/unit/view/dimension-marshal.spec.js @@ -3,7 +3,7 @@ import { getPreset } from '../../utils/dimension'; import createDimensionMarshal from '../../../src/state/dimension-marshal/dimension-marshal'; import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; import getArea from '../../../src/state/get-area'; -import * as state from '../../utils/simple-state-preset'; +import getStatePreset from '../../utils/get-simple-state-preset'; import type { Callbacks, DimensionMarshal, @@ -41,6 +41,7 @@ type PopulateMarshalState = {| |} const preset = getPreset(); +const state = getStatePreset(); const defaultArgs: PopulateMarshalState = { draggables: preset.draggables, diff --git a/test/unit/view/style-marshal.spec.js b/test/unit/view/style-marshal.spec.js index 5399b95f77..5bb36af768 100644 --- a/test/unit/view/style-marshal.spec.js +++ b/test/unit/view/style-marshal.spec.js @@ -1,10 +1,12 @@ // @flow import createStyleMarshal from '../../../src/view/style-marshal/style-marshal'; import getStyles, { type Styles } from '../../../src/view/style-marshal/get-styles'; +import getStatePreset from '../../utils/get-simple-state-preset'; import type { StyleMarshal } from '../../../src/view/style-marshal/style-marshal-types'; -import * as state from '../../utils/simple-state-preset'; import type { State } from '../../../src/types'; +const state = getStatePreset(); + const getStyleTagSelector = (context: string) => `style[data-react-beautiful-dnd="${context}"]`; diff --git a/test/utils/get-simple-state-preset.js b/test/utils/get-simple-state-preset.js index 1abd5f3d58..96e3bde1e8 100644 --- a/test/utils/get-simple-state-preset.js +++ b/test/utils/get-simple-state-preset.js @@ -1,7 +1,9 @@ // @flow import { getPreset } from './dimension'; import noImpact from '../../src/state/no-impact'; +import { vertical } from '../../src/state/axis'; import type { + Axis, State, DraggableDescriptor, DroppableDescriptor, @@ -19,7 +21,7 @@ import type { DragImpact, } from '../../src/types'; -export default (axis: Axis) => { +export default (axis?: Axis = vertical) => { const preset = getPreset(axis); const getDimensionState = (request: DraggableId): DimensionState => { From f19ce05619382bca8780bfe577a16bd0ef025441 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 15 Feb 2018 14:39:07 +1100 Subject: [PATCH 127/163] tests for everyone --- .../state/auto-scroll/fluid-scroller.spec.js | 85 ++++++++++++++++++- test/unit/state/dimension.spec.js | 8 +- .../get-best-cross-axis-droppable.spec.js | 42 +++++---- 3 files changed, 116 insertions(+), 19 deletions(-) diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index 6535eb7a9e..eecae1e13b 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -16,7 +16,7 @@ import getArea from '../../../../src/state/get-area'; import setViewport, { resetViewport } from '../../../utils/set-viewport'; import setWindowScrollSize, { resetWindowScrollSize } from '../../../utils/set-window-scroll-size'; import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-scroll'; -import { noMovement } from '../../../../src/state/no-impact'; +import noImpact, { noMovement } from '../../../../src/state/no-impact'; import { vertical, horizontal } from '../../../../src/state/axis'; import createAutoScroller from '../../../../src/state/auto-scroller/'; import getStatePreset from '../../../utils/get-simple-state-preset'; @@ -64,7 +64,7 @@ describe('fluid auto scrolling', () => { requestAnimationFrame.reset(); }); - [horizontal].forEach((axis: Axis) => { + [vertical, horizontal].forEach((axis: Axis) => { describe(`on the ${axis.direction} axis`, () => { const preset = getPreset(axis); const state = getStatePreset(axis); @@ -499,6 +499,7 @@ describe('fluid auto scrolling', () => { describe('droppable scrolling', () => { const thresholds: PixelThresholds = getPixelThresholds(frame, axis); + const maxScrollSpeed: Position = patch(axis.line, config.maxScrollSpeed); beforeEach(() => { // avoiding any window scrolling @@ -1061,7 +1062,87 @@ describe('fluid auto scrolling', () => { }); describe('over frame but not a subject', () => { + const withSmallSubject: DroppableDimension = getDroppableDimension({ + // stealing the home descriptor + descriptor: preset.home.descriptor, + direction: axis.direction, + client: getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + closest: { + frameClient: getArea({ + top: 0, + left: 0, + right: 5000, + bottom: 5000, + }), + scrollWidth: 10000, + scrollHeight: 10000, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + const endOfSubject: Position = patch(axis.line, 100); + const endOfFrame: Position = patch( + axis.line, + // on the end + 5000, + // half way + 2500, + ); + it.only('should scroll a frame if it is being dragged over, even if not over the subject', () => { + const scrolled: DroppableDimension = scrollDroppable( + withSmallSubject, + // scrolling the whole client away + endOfSubject, + ); + // subject no longer visible + expect(scrolled.viewport.clipped).toBe(null); + // const target: Position = add(endOfFrame, patch(axis.line, 1)); + + autoScroller.onStateChange( + state.idle, + withImpact( + addDroppable(dragTo(endOfFrame), scrolled), + // being super clear that we are not currently over any droppable + noImpact, + ) + ); + requestAnimationFrame.step(); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + maxScrollSpeed, + ); + }); + + it('should not scroll the frame if not over the frame', () => { + const scrolled: DroppableDimension = scrollDroppable( + withSmallSubject, + // scrolling the whole client away + endOfSubject, + ); + // subject no longer visible + expect(scrolled.viewport.clipped).toBe(null); + const target: Position = add(endOfFrame, patch(axis.line, 1)); + + autoScroller.onStateChange( + state.idle, + withImpact( + addDroppable(dragTo(target), scrolled), + // being super clear that we are not currently over any droppable + noImpact, + ) + ); + requestAnimationFrame.step(); + + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); }); }); diff --git a/test/unit/state/dimension.spec.js b/test/unit/state/dimension.spec.js index 76f5120656..cd43bc400d 100644 --- a/test/unit/state/dimension.spec.js +++ b/test/unit/state/dimension.spec.js @@ -447,7 +447,8 @@ describe('dimension', () => { })); }); - it('should increase the max scroll position if the requested scroll is bigger than the max', () => { + it('should allow scrolling beyond the max position', () => { + // this is to allow for scrolling into a foreign placeholder const scrollable: DroppableDimension = getDroppableDimension({ descriptor: droppableDescriptor, client: getArea({ @@ -472,7 +473,10 @@ describe('dimension', () => { const scrolled: DroppableDimension = scrollDroppable(scrollable, { x: 300, y: 300 }); - expect(getClosestScrollable(scrolled).scroll.max).toEqual({ x: 300, y: 300 }); + // current is larger than max + expect(getClosestScrollable(scrolled).scroll.current).toEqual({ x: 300, y: 300 }); + // current max is unchanged + expect(getClosestScrollable(scrolled).scroll.max).toEqual({ x: 100, y: 100 }); // original max expect(getClosestScrollable(scrollable).scroll.max).toEqual({ x: 100, y: 100 }); }); diff --git a/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js b/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js index 44b04fd93a..b3e6e52a84 100644 --- a/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js +++ b/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js @@ -309,14 +309,20 @@ describe('get best cross axis droppable', () => { // long droppable inside a shorter container - this should be clipped bottom: 80, }), - frameClient: getArea({ - // not the same top value as source - top: 20, - // shares the left edge with the source - left: 20, - right: 40, - bottom: 40, - }), + closest: { + frameClient: getArea({ + // not the same top value as source + top: 20, + // shares the left edge with the source + left: 20, + right: 40, + bottom: 40, + }), + scrollWidth: 20, + scrollHeight: 80, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const sibling2 = getDroppableDimension({ descriptor: { @@ -635,13 +641,19 @@ describe('get best cross axis droppable', () => { [axis.crossAxisStart]: 200, [axis.crossAxisEnd]: 300, }), - frameClient: getArea({ - [axis.start]: 0, - [axis.end]: 100, - // frame hides subject - [axis.crossAxisStart]: 400, - [axis.crossAxisEnd]: 500, - }), + closest: { + frameClient: getArea({ + [axis.start]: 0, + [axis.end]: 100, + // frame hides subject + [axis.crossAxisStart]: 400, + [axis.crossAxisEnd]: 500, + }), + scroll: { x: 0, y: 0 }, + scrollWidth: 100, + scrollHeight: 100, + shouldClipSubject: true, + }, }); const droppables: DroppableDimensionMap = { [source.descriptor.id]: source, From ff59db02459704cb412c1cbaf2ee6f9294d36d23 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 15 Feb 2018 15:51:43 +1100 Subject: [PATCH 128/163] get-closest-draggable tests --- .../move-cross-axis/get-closest-draggable.js | 4 +- .../get-closest-draggable.spec.js | 527 ++++++++++-------- test/utils/dimension.js | 19 +- 3 files changed, 303 insertions(+), 247 deletions(-) diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index 6ea0f76415..8485ad91d9 100644 --- a/src/state/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-cross-axis/get-closest-draggable.js @@ -1,5 +1,5 @@ // @flow -import { add, distance } from '../position'; +import { distance } from '../position'; import getViewport from '../../window/get-viewport'; import { isTotallyVisible } from '../visibility/is-visible'; import withDroppableDisplacement from '../with-droppable-displacement'; @@ -20,8 +20,6 @@ type Args = {| insideDestination: DraggableDimension[], |} -const origin: Position = { x: 0, y: 0 }; - export default ({ axis, pageCenter, diff --git a/test/unit/state/move-cross-axis/get-closest-draggable.spec.js b/test/unit/state/move-cross-axis/get-closest-draggable.spec.js index 991e862055..4b9b761a0c 100644 --- a/test/unit/state/move-cross-axis/get-closest-draggable.spec.js +++ b/test/unit/state/move-cross-axis/get-closest-draggable.spec.js @@ -1,11 +1,13 @@ // @flow import getClosestDraggable from '../../../../src/state/move-cross-axis/get-closest-draggable'; -import { getDroppableDimension, getDraggableDimension } from '../../../../src/state/dimension'; +import { getDroppableDimension, getDraggableDimension, scrollDroppable } from '../../../../src/state/dimension'; import { add, distance, patch } from '../../../../src/state/position'; +import { expandByPosition } from '../../../../src/state/spacing'; import { horizontal, vertical } from '../../../../src/state/axis'; import getArea from '../../../../src/state/get-area'; import getViewport from '../../../../src/window/get-viewport'; import type { + Area, Axis, Position, DraggableDimension, @@ -14,308 +16,359 @@ import type { describe('get closest draggable', () => { [vertical, horizontal].forEach((axis: Axis) => { - const start: number = 0; - const end: number = 100; - const crossAxisStart: number = 0; - const crossAxisEnd: number = 20; - - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'droppable', - type: 'TYPE', - }, - client: getArea({ + describe(`on the ${axis.direction} axis`, () => { + const start: number = 0; + const end: number = 100; + const crossAxisStart: number = 0; + const crossAxisEnd: number = 20; + + const client: Area = getArea({ [axis.start]: start, [axis.end]: end, [axis.crossAxisStart]: crossAxisStart, [axis.crossAxisEnd]: crossAxisEnd, - }), - }); - - const hiddenBackwards: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'hiddenBackwards', - droppableId: droppable.descriptor.id, - index: 0, - }, - client: getArea({ - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: -30, // -10 - [axis.end]: -10, - }), - }); - - // item bleeds backwards past the start of the droppable - const partiallyHiddenBackwards: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'partialHiddenBackwards', - droppableId: droppable.descriptor.id, - index: 1, - }, - client: getArea({ - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: -10, // -10 - [axis.end]: 20, - }), - }); - - const visible1: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'visible1', - droppableId: droppable.descriptor.id, - index: 2, - }, - client: getArea({ - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 20, - [axis.end]: 40, - }), - }); + }); - const visible2: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'visible2', - droppableId: droppable.descriptor.id, - index: 3, - }, - client: getArea({ - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 40, - [axis.end]: 60, - }), - }); + const droppable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'droppable', + type: 'TYPE', + }, + direction: axis.direction, + client, + }); - // bleeds over the end of the visible boundary - const partiallyHiddenForwards: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'partiallyHiddenForwards', - droppableId: droppable.descriptor.id, - index: 4, - }, - client: getArea({ - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 60, - [axis.end]: 120, - }), - }); + const hiddenBackwards: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'hiddenBackwards', + droppableId: droppable.descriptor.id, + index: 0, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: -30, // -10 + [axis.end]: -10, + }), + }); - // totally invisible - const hiddenForwards: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'hiddenForwards', - droppableId: droppable.descriptor.id, - index: 5, - }, - client: getArea({ - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 120, - [axis.end]: 140, - }), - }); + // item bleeds backwards past the start of the droppable + const partiallyHiddenBackwards: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'partialHiddenBackwards', + droppableId: droppable.descriptor.id, + index: 1, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: -10, // -10 + [axis.end]: 20, + }), + }); - const viewport = getViewport(); - const outOfViewport: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'hidden', - droppableId: droppable.descriptor.id, - index: 6, - }, - client: getArea({ - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: viewport[axis.end] + 1, - [axis.end]: viewport[axis.end] + 10, - }), - }); + const visible1: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'visible1', + droppableId: droppable.descriptor.id, + index: 2, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 20, + [axis.end]: 40, + }), + }); - const insideDestination: DraggableDimension[] = [ - hiddenBackwards, - partiallyHiddenBackwards, - visible1, - visible2, - partiallyHiddenForwards, - hiddenForwards, - outOfViewport, - ]; - - it('should return the closest draggable', () => { - // closet to visible1 - const center1: Position = patch( - axis.line, visible1.page.withoutMargin.center[axis.line], 100 - ); - const result1: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center1, - destination: droppable, - insideDestination, + const visible2: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'visible2', + droppableId: droppable.descriptor.id, + index: 3, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 40, + [axis.end]: 60, + }), }); - expect(result1).toBe(visible1); - - // closest to visible2 - const center2: Position = patch( - axis.line, visible2.page.withoutMargin.center[axis.line], 100 - ); - const result2: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center2, - destination: droppable, - insideDestination, + + // bleeds over the end of the visible boundary + const partiallyHiddenForwards: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'partiallyHiddenForwards', + droppableId: droppable.descriptor.id, + index: 4, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 60, + [axis.end]: 120, + }), }); - expect(result2).toBe(visible2); - }); - it('should return null if there are no draggables in the droppable', () => { - const center: Position = { - x: 100, - y: 100, - }; - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: droppable, - insideDestination: [], + // totally invisible + const hiddenForwards: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'hiddenForwards', + droppableId: droppable.descriptor.id, + index: 5, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 120, + [axis.end]: 140, + }), }); - expect(result).toBe(null); - }); + const viewport = getViewport(); + const outOfViewport: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'hidden', + droppableId: droppable.descriptor.id, + index: 6, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: viewport[axis.end] + 1, + [axis.end]: viewport[axis.end] + 10, + }), + }); - describe('removal of draggables that are visible', () => { - it('should ignore draggables backward that have no visiblity', () => { - const center: Position = patch( - axis.line, hiddenBackwards.page.withoutMargin.center[axis.line], 100 + const insideDestination: DraggableDimension[] = [ + hiddenBackwards, + partiallyHiddenBackwards, + visible1, + visible2, + partiallyHiddenForwards, + hiddenForwards, + outOfViewport, + ]; + + it('should return the closest draggable', () => { + // closet to visible1 + const center1: Position = patch( + axis.line, visible1.page.withoutMargin.center[axis.line], 100 ); - - const result: ?DraggableDimension = getClosestDraggable({ + const result1: ?DraggableDimension = getClosestDraggable({ axis, - pageCenter: center, + pageCenter: center1, destination: droppable, insideDestination, }); + expect(result1).toBe(visible1); - expect(result).toBe(partiallyHiddenBackwards); - }); - - it('should not ignore draggables that have backwards partial visiblility', () => { - const center: Position = patch( - axis.line, partiallyHiddenBackwards.page.withoutMargin.center[axis.line], 100 + // closest to visible2 + const center2: Position = patch( + axis.line, visible2.page.withoutMargin.center[axis.line], 100 ); - - const result: ?DraggableDimension = getClosestDraggable({ + const result2: ?DraggableDimension = getClosestDraggable({ axis, - pageCenter: center, + pageCenter: center2, destination: droppable, insideDestination, }); - - expect(result).toBe(partiallyHiddenBackwards); + expect(result2).toBe(visible2); }); - it('should not ignore draggables that have forward partial visiblility', () => { - const center: Position = patch( - axis.line, partiallyHiddenForwards.page.withoutMargin.center[axis.line], 100 - ); + it('should return null if there are no draggables in the droppable', () => { + const center: Position = { + x: 100, + y: 100, + }; const result: ?DraggableDimension = getClosestDraggable({ axis, pageCenter: center, destination: droppable, - insideDestination, + insideDestination: [], }); - expect(result).toBe(partiallyHiddenForwards); + expect(result).toBe(null); }); - it('should ignore draggables forward that have no visiblity', () => { + it('should take into account the change in droppable scroll', () => { + const scrollable: DroppableDimension = getDroppableDimension({ + descriptor: droppable.descriptor, + direction: axis.direction, + client, + closest: { + frameClient: getArea(expandByPosition(client, patch(axis.line, 100))), + scrollHeight: client.width + 100, + scrollWidth: client.height + 100, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + patch(axis.line, 20) + ); const center: Position = patch( - axis.line, hiddenForwards.page.withoutMargin.center[axis.line], 100 + axis.line, + visible1.page.withoutMargin.center[axis.line], + 100 ); const result: ?DraggableDimension = getClosestDraggable({ axis, pageCenter: center, - destination: droppable, + destination: scrolled, insideDestination, }); - expect(result).toBe(partiallyHiddenForwards); - }); - - it('should ignore draggables that are outside of the viewport', () => { - const center: Position = patch( - axis.line, outOfViewport.page.withoutMargin.center[axis.line], 100 - ); + expect(result).toBe(visible2); - const result: ?DraggableDimension = getClosestDraggable({ + // validation - with no scroll applied we are normally closer to visible1 + const result1: ?DraggableDimension = getClosestDraggable({ axis, pageCenter: center, destination: droppable, insideDestination, }); + expect(result1).toBe(visible1); + }); + + describe('removal of draggables that are visible', () => { + it('should ignore draggables backward that have no total visiblity', () => { + const center: Position = patch( + axis.line, + hiddenBackwards.page.withoutMargin.center[axis.line], + 100, + ); + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + insideDestination, + }); + + expect(result).toBe(visible1); + }); + + it('should ignore draggables that have backwards partial visiblility', () => { + const center: Position = patch( + axis.line, + partiallyHiddenBackwards.page.withoutMargin.center[axis.line], + 100, + ); + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + insideDestination, + }); + + expect(result).toBe(visible1); + }); + + it('should ignore draggables that have forward partial visiblility', () => { + const center: Position = patch( + axis.line, partiallyHiddenForwards.page.withoutMargin.center[axis.line], 100 + ); + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + insideDestination, + }); + + expect(result).toBe(visible2); + }); + + it('should ignore draggables forward that have no visiblity', () => { + const center: Position = patch( + axis.line, hiddenForwards.page.withoutMargin.center[axis.line], 100 + ); + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + insideDestination, + }); + + expect(result).toBe(visible2); + }); + + it('should ignore draggables that are outside of the viewport', () => { + const center: Position = patch( + axis.line, outOfViewport.page.withoutMargin.center[axis.line], 100 + ); + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + insideDestination, + }); + + expect(result).toBe(visible2); + }); - expect(result).toBe(partiallyHiddenForwards); + it('should return null if there are no visible targets', () => { + const notVisible: DraggableDimension[] = [ + hiddenBackwards, + hiddenForwards, + outOfViewport, + ]; + const center: Position = { + x: 0, + y: 0, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + insideDestination: notVisible, + }); + + expect(result).toBe(null); + }); }); - it('should return null if there are no visible targets', () => { - const notVisible: DraggableDimension[] = [ - hiddenBackwards, - hiddenForwards, - outOfViewport, - ]; - const center: Position = { - x: 0, - y: 0, - }; + it('should return the draggable that is first on the main axis in the event of a tie', () => { + // in this case the distance between visible1 and visible2 is the same + const center: Position = patch( + axis.line, + // this is shared edge + visible2.page.withoutMargin[axis.start], + 100 + ); const result: ?DraggableDimension = getClosestDraggable({ axis, pageCenter: center, destination: droppable, - insideDestination: notVisible, + insideDestination, }); - expect(result).toBe(null); - }); - }); - - it('should return the draggable that is first on the main axis in the event of a tie', () => { - // in this case the distance between visible1 and visible2 is the same - const center: Position = patch( - axis.line, - // this is shared edge - visible2.page.withoutMargin[axis.start], - 100 - ); - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: droppable, - insideDestination, - }); + expect(result).toBe(visible1); - expect(result).toBe(visible1); + // validating test assumptions - // validating test assumptions + // 1. that they have equal distances + expect(distance(center, visible1.page.withoutMargin.center)) + .toEqual(distance(center, visible2.page.withoutMargin.center)); - // 1. that they have equal distances - expect(distance(center, visible1.page.withoutMargin.center)) - .toEqual(distance(center, visible2.page.withoutMargin.center)); - - // 2. if we move beyond the edge visible2 will be selected - const result2: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: add(center, patch(axis.line, 1)), - destination: droppable, - insideDestination, + // 2. if we move beyond the edge visible2 will be selected + const result2: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: add(center, patch(axis.line, 1)), + destination: droppable, + insideDestination, + }); + expect(result2).toBe(visible2); }); - expect(result2).toBe(visible2); }); }); }); diff --git a/test/utils/dimension.js b/test/utils/dimension.js index e29c7c9ba6..e28d8b39f9 100644 --- a/test/utils/dimension.js +++ b/test/utils/dimension.js @@ -30,20 +30,25 @@ const emptyForeignCrossAxisEnd: number = 300; export const makeScrollable = (droppable: DroppableDimension, amount?: number = 20) => { const axis: Axis = droppable.axis; const client: Area = droppable.client.withoutMargin; + + const horizontalGrowth: number = axis === vertical ? 0 : amount; + const verticalGrowth: number = axis === vertical ? amount : 0; + // is 10px smaller than the client on the main axis // this will leave 10px of scrollable area. // only expanding on one axis - const frameClient: Area = getArea({ + const newClient: Area = getArea({ top: client.top, left: client.left, - right: axis === vertical ? client.right : client.right - amount, - bottom: axis === vertical ? client.bottom - amount : client.bottom, + // growing the client to account for the scrollable area + right: client.right + horizontalGrowth, + bottom: client.bottom + verticalGrowth, }); // add scroll space on the main axis const scrollSize = { - width: axis === vertical ? client.width : client.width + amount, - height: axis === vertical ? client.height + amount : client.height, + width: client.width + horizontalGrowth, + height: client.height + verticalGrowth, }; return getDroppableDimension({ @@ -52,9 +57,9 @@ export const makeScrollable = (droppable: DroppableDimension, amount?: number = padding, margin, windowScroll, - client, + client: newClient, closest: { - frameClient, + frameClient: client, scrollWidth: scrollSize.width, scrollHeight: scrollSize.height, scroll: { x: 0, y: 0 }, From 48bf586dfefa75e0825d7b2cb31aa96449e36b01 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 15 Feb 2018 16:56:48 +1100 Subject: [PATCH 129/163] testing scroll displacement in move-to-new-droppable --- .../move-to-new-droppable/to-foreign-list.js | 2 +- .../move-to-new-droppable.spec.js | 825 ++++++++++++------ 2 files changed, 541 insertions(+), 286 deletions(-) diff --git a/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js index 65c5604933..b1552e39a5 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js +++ b/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js @@ -65,7 +65,7 @@ export default ({ }; return { - pageCenter: newCenter, + pageCenter: withDroppableDisplacement(droppable, newCenter), impact: newImpact, }; } diff --git a/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js b/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js index e1ce3466b1..36af179307 100644 --- a/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js +++ b/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js @@ -1,12 +1,12 @@ // @flow import moveToNewDroppable from '../../../../src/state/move-cross-axis/move-to-new-droppable/'; import type { Result } from '../../../../src/state/move-cross-axis/move-cross-axis-types'; -import { getDraggableDimension, getDroppableDimension } from '../../../../src/state/dimension'; +import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; import getArea from '../../../../src/state/get-area'; import moveToEdge from '../../../../src/state/move-to-edge'; -import { patch } from '../../../../src/state/position'; +import { add, negate, patch } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; -import { getPreset } from '../../../utils/dimension'; +import { getPreset, makeScrollable } from '../../../utils/dimension'; import noImpact from '../../../../src/state/no-impact'; import getViewport from '../../../../src/window/get-viewport'; import type { @@ -86,129 +86,217 @@ describe('move to new droppable', () => { }); describe('moving back into original index', () => { - // the second draggable is moving back into its home - const result: ?Result = moveToNewDroppable({ - pageCenter: dontCare, - draggable: inHome2, - target: inHome2, - destination: home, - insideDestination: draggables, - home: { - index: 1, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - }); + describe('without droppable scroll', () => { + // the second draggable is moving back into its home + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: inHome2, + target: inHome2, + destination: home, + insideDestination: draggables, + home: { + index: 1, + droppableId: home.descriptor.id, + }, + previousImpact: noImpact, + }); - if (!result) { - throw new Error('invalid test setup'); - } + if (!result) { + throw new Error('invalid test setup'); + } + + it('should return the original center without margin', () => { + expect(result.pageCenter).toBe(inHome2.page.withoutMargin.center); + expect(result.pageCenter).not.toEqual(inHome2.page.withMargin.center); + }); - it('should return the original center without margin', () => { - expect(result.pageCenter).toBe(inHome2.page.withoutMargin.center); - expect(result.pageCenter).not.toEqual(inHome2.page.withMargin.center); + it('should return an empty impact with the original location', () => { + const expected: DragImpact = { + movement: { + displaced: [], + amount: patch(axis.line, inHome2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: home.descriptor.id, + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should return an empty impact with the original location', () => { - const expected: DragImpact = { - movement: { - displaced: [], - amount: patch(axis.line, inHome2.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, + describe('with droppable scroll', () => { + const scrollable: DroppableDimension = makeScrollable(home, 10); + const scroll: Position = patch(axis.line, 10); + const displacement: Position = negate(scroll); + const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10)); + + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: inHome2, + target: inHome2, + destination: scrolled, + insideDestination: draggables, + home: { index: 1, + droppableId: home.descriptor.id, }, - }; + previousImpact: noImpact, + }); - expect(result.impact).toEqual(expected); + if (!result) { + throw new Error('Invalid result'); + } + + it('should account for changes in droppable scroll', () => { + const expected: Position = add(inHome2.page.withoutMargin.center, displacement); + + expect(result.pageCenter).toEqual(expected); + }); + + it('should return an empty impact with the original location', () => { + const expected: DragImpact = { + movement: { + displaced: [], + amount: patch(axis.line, inHome2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: home.descriptor.id, + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); }); describe('moving before the original index', () => { - // moving inHome4 into the inHome2 position - const result: ?Result = moveToNewDroppable({ - pageCenter: dontCare, - draggable: inHome4, - target: inHome2, - destination: home, - insideDestination: draggables, - home: { - index: 3, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - }); + describe('without droppable scroll', () => { + // moving inHome4 into the inHome2 position + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: inHome4, + target: inHome2, + destination: home, + insideDestination: draggables, + home: { + index: 3, + droppableId: home.descriptor.id, + }, + previousImpact: noImpact, + }); - if (!result) { - throw new Error('invalid test setup'); - } + if (!result) { + throw new Error('invalid test setup'); + } + + it('should align to the start of the target', () => { + const expected: Position = moveToEdge({ + source: inHome4.page.withoutMargin, + sourceEdge: 'start', + destination: inHome2.page.withMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); - it('should align to the start of the target', () => { - const expected: Position = moveToEdge({ - source: inHome4.page.withoutMargin, - sourceEdge: 'start', - destination: inHome2.page.withMargin, - destinationEdge: 'start', - destinationAxis: axis, + expect(result.pageCenter).toEqual(expected); }); - expect(result.pageCenter).toEqual(expected); + it('should move the everything from the target index to the original index forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + displaced: [ + { + draggableId: inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ], + amount: patch(axis.line, inHome4.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: home.descriptor.id, + // original index of target + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should move the everything from the target index to the original index forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - displaced: [ - { - draggableId: inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch(axis.line, inHome4.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { + describe('with droppable scroll', () => { + const scrollable: DroppableDimension = makeScrollable(home, 10); + const scroll: Position = patch(axis.line, 10); + const displacement: Position = negate(scroll); + const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10)); + + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: inHome4, + target: inHome2, + destination: scrolled, + insideDestination: draggables, + home: { + index: 3, droppableId: home.descriptor.id, - // original index of target - index: 1, }, - }; + previousImpact: noImpact, + }); - expect(result.impact).toEqual(expected); + if (!result) { + throw new Error('Invalid result'); + } + + it('should account for changes in droppable scroll', () => { + const withoutScroll: Position = moveToEdge({ + source: inHome4.page.withoutMargin, + sourceEdge: 'start', + destination: inHome2.page.withMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); + const expected: Position = add(withoutScroll, displacement); + + expect(result.pageCenter).toEqual(expected); + }); }); }); describe('moving after the original index', () => { - // moving inHome1 into the inHome4 position - const result: ?Result = moveToNewDroppable({ - pageCenter: dontCare, - draggable: inHome1, - target: inHome4, - destination: home, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - }); + describe('without droppable scroll', () => { + // moving inHome1 into the inHome4 position + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: inHome1, + target: inHome4, + destination: home, + insideDestination: draggables, + home: { + index: 0, + droppableId: home.descriptor.id, + }, + previousImpact: noImpact, + }); - if (!result) { - throw new Error('invalid test setup'); - } + if (!result) { + throw new Error('invalid test setup'); + } - describe('center', () => { it('should align to the bottom of the target', () => { const expected: Position = moveToEdge({ source: inHome1.page.withoutMargin, @@ -220,42 +308,79 @@ describe('move to new droppable', () => { expect(result.pageCenter).toEqual(expected); }); + + it('should move the everything from the target index to the original index forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + displaced: [ + { + draggableId: inHome4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ], + amount: patch(axis.line, inHome1.page.withMargin[axis.size]), + // is moving beyond start position + isBeyondStartPosition: true, + }, + direction: axis.direction, + destination: { + droppableId: home.descriptor.id, + // original index of target + index: 3, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should move the everything from the target index to the original index forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - displaced: [ - { - draggableId: inHome4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch(axis.line, inHome1.page.withMargin[axis.size]), - // is moving beyond start position - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { + describe('with droppable scroll', () => { + const scrollable: DroppableDimension = makeScrollable(home, 10); + const scroll: Position = patch(axis.line, 10); + const displacement: Position = negate(scroll); + const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10)); + + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: inHome1, + target: inHome4, + destination: scrolled, + insideDestination: draggables, + home: { + index: 0, droppableId: home.descriptor.id, - // original index of target - index: 3, }, - }; + previousImpact: noImpact, + }); - expect(result.impact).toEqual(expected); + if (!result) { + throw new Error('Invalid result'); + } + + it('should account for changes in droppable scroll', () => { + const withoutScroll: Position = moveToEdge({ + source: inHome1.page.withoutMargin, + sourceEdge: 'end', + destination: inHome4.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + const expected: Position = add(withoutScroll, displacement); + + expect(result.pageCenter).toEqual(expected); + }); }); }); @@ -274,13 +399,19 @@ describe('move to new droppable', () => { // will be cut by frame [axis.end]: 200, }), - frameClient: getArea({ - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - // will cut the subject - [axis.end]: 100, - }), + closest: { + frameClient: getArea({ + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + // will cut the subject + [axis.end]: 100, + }), + scrollWidth: 200, + scrollHeight: 200, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const inside: DraggableDimension = getDraggableDimension({ descriptor: { @@ -461,180 +592,298 @@ describe('move to new droppable', () => { }); describe('moving into an unpopulated list', () => { - const result: ?Result = moveToNewDroppable({ - pageCenter: inHome1.page.withMargin.center, - draggable: inHome1, - target: null, - destination: foreign, - insideDestination: [], - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - }); + describe('without droppable scroll', () => { + const result: ?Result = moveToNewDroppable({ + pageCenter: inHome1.page.withMargin.center, + draggable: inHome1, + target: null, + destination: foreign, + insideDestination: [], + home: { + index: 0, + droppableId: home.descriptor.id, + }, + previousImpact: noImpact, + }); - if (!result) { - throw new Error('invalid test setup'); - } + if (!result) { + throw new Error('invalid test setup'); + } + + it('should move to the start edge of the droppable (including its padding)', () => { + const expected: Position = moveToEdge({ + source: inHome1.page.withoutMargin, + sourceEdge: 'start', + destination: foreign.page.withMarginAndPadding, + destinationEdge: 'start', + destinationAxis: foreign.axis, + }); - it('should move to the start edge of the droppable (including its padding)', () => { - const expected: Position = moveToEdge({ - source: inHome1.page.withoutMargin, - sourceEdge: 'start', - destination: foreign.page.withMarginAndPadding, - destinationEdge: 'start', - destinationAxis: foreign.axis, + expect(result.pageCenter).toEqual(expected); }); - expect(result.pageCenter).toEqual(expected); + it('should return an empty impact', () => { + const expected: DragImpact = { + movement: { + displaced: [], + amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), + isBeyondStartPosition: false, + }, + direction: foreign.axis.direction, + destination: { + droppableId: foreign.descriptor.id, + index: 0, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should return an empty impact', () => { - const expected: DragImpact = { - movement: { - displaced: [], - amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), - isBeyondStartPosition: false, - }, - direction: foreign.axis.direction, - destination: { - droppableId: foreign.descriptor.id, + describe('with droppable scroll', () => { + const scrollable: DroppableDimension = makeScrollable(foreign, 10); + const scroll: Position = patch(axis.line, 10); + const displacement: Position = negate(scroll); + const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10)); + + const result: ?Result = moveToNewDroppable({ + pageCenter: inHome1.page.withMargin.center, + draggable: inHome1, + target: null, + destination: scrolled, + insideDestination: [], + home: { index: 0, + droppableId: home.descriptor.id, }, - }; + previousImpact: noImpact, + }); - expect(result.impact).toEqual(expected); + if (!result) { + throw new Error('Invalid result'); + } + + it('should account for changes in droppable scroll', () => { + const withoutScroll: Position = moveToEdge({ + source: inHome1.page.withoutMargin, + sourceEdge: 'start', + destination: foreign.page.withMarginAndPadding, + destinationEdge: 'start', + destinationAxis: foreign.axis, + }); + const expected: Position = add(withoutScroll, displacement); + + expect(result.pageCenter).toEqual(expected); + }); }); }); describe('is moving before the target', () => { - // moving home1 into the second position of the list - const result: ?Result = moveToNewDroppable({ - pageCenter: inHome1.page.withMargin.center, - draggable: inHome1, - target: inForeign2, - destination: foreign, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - }); + describe('without droppable scroll', () => { + // moving home1 into the second position of the list + const result: ?Result = moveToNewDroppable({ + pageCenter: inHome1.page.withMargin.center, + draggable: inHome1, + target: inForeign2, + destination: foreign, + insideDestination: draggables, + home: { + index: 0, + droppableId: home.descriptor.id, + }, + previousImpact: noImpact, + }); - if (!result) { - throw new Error('invalid test setup'); - } + if (!result) { + throw new Error('invalid test setup'); + } + + it('should move before the target', () => { + const expected: Position = moveToEdge({ + source: inHome1.page.withoutMargin, + sourceEdge: 'start', + destination: inForeign2.page.withMargin, + destinationEdge: 'start', + destinationAxis: foreign.axis, + }); - it('should move before the target', () => { - const expected: Position = moveToEdge({ - source: inHome1.page.withoutMargin, - sourceEdge: 'start', - destination: inForeign2.page.withMargin, - destinationEdge: 'start', - destinationAxis: foreign.axis, + expect(result.pageCenter).toEqual(expected); }); - expect(result.pageCenter).toEqual(expected); + it('should move the target and everything below it forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + displaced: [ + { + draggableId: inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ], + amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), + isBeyondStartPosition: false, + }, + direction: foreign.axis.direction, + destination: { + droppableId: foreign.descriptor.id, + // index of foreign2 + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should move the target and everything below it forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - displaced: [ - { - draggableId: inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), - isBeyondStartPosition: false, - }, - direction: foreign.axis.direction, - destination: { - droppableId: foreign.descriptor.id, - // index of foreign2 - index: 1, + describe('with droppable scroll', () => { + const scrollable: DroppableDimension = makeScrollable(foreign, 10); + const scroll: Position = patch(axis.line, 10); + const displacement: Position = negate(scroll); + const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10)); + + const result: ?Result = moveToNewDroppable({ + pageCenter: inHome1.page.withMargin.center, + draggable: inHome1, + target: inForeign2, + destination: scrolled, + insideDestination: draggables, + home: { + index: 0, + droppableId: home.descriptor.id, }, - }; + previousImpact: noImpact, + }); - expect(result.impact).toEqual(expected); + if (!result) { + throw new Error('Invalid result'); + } + + it('should account for changes in droppable scroll', () => { + const withoutScroll: Position = moveToEdge({ + source: inHome1.page.withoutMargin, + sourceEdge: 'start', + destination: inForeign2.page.withMargin, + destinationEdge: 'start', + destinationAxis: foreign.axis, + }); + const expected: Position = add(withoutScroll, displacement); + + expect(result.pageCenter).toEqual(expected); + }); }); }); describe('is moving after the target', () => { - // moving home4 into the second position of the foreign list - const result: ?Result = moveToNewDroppable({ - pageCenter: inHome4.page.withMargin.center, - draggable: inHome4, - target: inForeign2, - destination: foreign, - insideDestination: draggables, - home: { - index: 3, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - }); + describe('without droppable scroll', () => { + // moving home4 into the second position of the foreign list + const result: ?Result = moveToNewDroppable({ + pageCenter: inHome4.page.withMargin.center, + draggable: inHome4, + target: inForeign2, + destination: foreign, + insideDestination: draggables, + home: { + index: 3, + droppableId: home.descriptor.id, + }, + previousImpact: noImpact, + }); - if (!result) { - throw new Error('invalid test setup'); - } + if (!result) { + throw new Error('invalid test setup'); + } - it('should move after the target', () => { - const expected = moveToEdge({ - source: inHome4.page.withoutMargin, - sourceEdge: 'start', - destination: inForeign2.page.withMargin, - // going after - destinationEdge: 'end', - destinationAxis: foreign.axis, + it('should move after the target', () => { + const expected = moveToEdge({ + source: inHome4.page.withoutMargin, + sourceEdge: 'start', + destination: inForeign2.page.withMargin, + // going after + destinationEdge: 'end', + destinationAxis: foreign.axis, + }); + + expect(result.pageCenter).toEqual(expected); }); - expect(result.pageCenter).toEqual(expected); + it('should move everything after the proposed index forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + displaced: [ + { + draggableId: inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ], + amount: patch(foreign.axis.line, inHome4.page.withMargin[foreign.axis.size]), + isBeyondStartPosition: false, + }, + direction: foreign.axis.direction, + destination: { + droppableId: foreign.descriptor.id, + // going after target, so index is target index + 1 + index: 2, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should move everything after the proposed index forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - displaced: [ - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch(foreign.axis.line, inHome4.page.withMargin[foreign.axis.size]), - isBeyondStartPosition: false, - }, - direction: foreign.axis.direction, - destination: { - droppableId: foreign.descriptor.id, - // going after target, so index is target index + 1 - index: 2, + describe('with droppable scroll', () => { + const scrollable: DroppableDimension = makeScrollable(foreign, 10); + const scroll: Position = patch(axis.line, 10); + const displacement: Position = negate(scroll); + const scrolled: DroppableDimension = scrollDroppable(scrollable, patch(axis.line, 10)); + + const result: ?Result = moveToNewDroppable({ + pageCenter: inHome4.page.withMargin.center, + draggable: inHome4, + target: inForeign2, + destination: scrolled, + insideDestination: draggables, + home: { + index: 3, + droppableId: home.descriptor.id, }, - }; + previousImpact: noImpact, + }); - expect(result.impact).toEqual(expected); + if (!result) { + throw new Error('Invalid result'); + } + + it('should account for changes in droppable scroll', () => { + const withoutScroll: Position = moveToEdge({ + source: inHome4.page.withoutMargin, + sourceEdge: 'start', + destination: inForeign2.page.withMargin, + // going after + destinationEdge: 'end', + destinationAxis: foreign.axis, + }); + const expected: Position = add(withoutScroll, displacement); + + expect(result.pageCenter).toEqual(expected); + }); }); }); @@ -679,12 +928,18 @@ describe('move to new droppable', () => { // will be cut by frame bottom: 200, }), - frameClient: getArea({ - top: 0, - left: 0, - right: 100, - bottom: 100, - }), + closest: { + frameClient: getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + scrollWidth: 200, + scrollHeight: 200, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const customInForeign: DraggableDimension = getDraggableDimension({ From 183bfd72370f3bdb68c1e4eb7cbc3e22225b7bab Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 15 Feb 2018 16:59:44 +1100 Subject: [PATCH 130/163] fixing import --- .../draggable-dimension-publisher.jsx | 4 ++-- src/view/draggable/draggable.jsx | 8 ++++---- test/unit/view/drag-handle.spec.js | 4 ++-- test/unit/view/unconnected-draggable.spec.js | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx b/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx index d25e2dd410..2f97b60151 100644 --- a/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx +++ b/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx @@ -3,7 +3,7 @@ import { Component } from 'react'; import type { Node } from 'react'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; -import getWindowScrollPosition from '../../window/get-window-scroll'; +import getWindowScroll from '../../window/get-window-scroll'; import { getDraggableDimension } from '../../state/dimension'; import { dimensionMarshalKey } from '../context-keys'; import getArea from '../../state/get-area'; @@ -119,7 +119,7 @@ export default class DraggableDimensionPublisher extends Component { descriptor, client, margin, - windowScroll: getWindowScrollPosition(), + windowScroll: getWindowScroll(), }); return dimension; diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 6e59bca416..2e1597156a 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -13,7 +13,7 @@ import type { import DraggableDimensionPublisher from '../draggable-dimension-publisher/'; import Moveable from '../moveable/'; import DragHandle from '../drag-handle'; -import getWindowScrollPosition from '../../window/get-window-scroll'; +import getWindowScroll from '../../window/get-window-scroll'; // eslint-disable-next-line no-duplicate-imports import type { DragHandleProps, @@ -118,7 +118,7 @@ export default class Draggable extends Component { center: getCenterPosition(ref), }; - const windowScroll: Position = getWindowScrollPosition(); + const windowScroll: Position = getWindowScroll(); lift(draggableId, initial, windowScroll, autoScrollMode); } @@ -133,7 +133,7 @@ export default class Draggable extends Component { return; } - const windowScroll: Position = getWindowScrollPosition(); + const windowScroll: Position = getWindowScroll(); move(draggableId, client, windowScroll); } @@ -160,7 +160,7 @@ export default class Draggable extends Component { onWindowScroll = () => { this.throwIfCannotDrag(); - const windowScroll = getWindowScrollPosition(); + const windowScroll = getWindowScroll(); this.props.moveByWindowScroll(this.props.draggableId, windowScroll); } diff --git a/test/unit/view/drag-handle.spec.js b/test/unit/view/drag-handle.spec.js index 3a3bf2b947..c32078924c 100644 --- a/test/unit/view/drag-handle.spec.js +++ b/test/unit/view/drag-handle.spec.js @@ -18,7 +18,7 @@ import { } from '../../utils/user-input-util'; import type { Position, DraggableId } from '../../../src/types'; import * as keyCodes from '../../../src/view/key-codes'; -import getWindowScrollPosition from '../../../src/view/get-window-scroll-position'; +import getWindowScroll from '../../../src/window/get-window-scroll'; import setWindowScroll from '../../utils/set-window-scroll'; import forceUpdate from '../../utils/force-update'; import getArea from '../../../src/state/get-area'; @@ -515,7 +515,7 @@ describe('drag handle', () => { }); describe('window scroll during drag', () => { - const originalScroll: Position = getWindowScrollPosition(); + const originalScroll: Position = getWindowScroll(); beforeEach(() => { setWindowScroll(origin, { shouldPublish: false }); diff --git a/test/unit/view/unconnected-draggable.spec.js b/test/unit/view/unconnected-draggable.spec.js index f1ebf028ee..0573d58467 100644 --- a/test/unit/view/unconnected-draggable.spec.js +++ b/test/unit/view/unconnected-draggable.spec.js @@ -37,7 +37,7 @@ import getArea from '../../../src/state/get-area'; import { combine, withStore, withDroppableId, withStyleContext, withDimensionMarshal, withCanLift } from '../../utils/get-context-options'; import { dispatchWindowMouseEvent, mouseEvent } from '../../utils/user-input-util'; import setWindowScroll from '../../utils/set-window-scroll'; -import getWindowScrollPosition from '../../../src/view/get-window-scroll-position'; +import getWindowScroll from '../../../src/window/get-window-scroll'; class Item extends Component<{ provided: Provided }> { render() { @@ -206,7 +206,7 @@ const mountDraggable = ({ const mouseDown = mouseEvent.bind(null, 'mousedown'); const windowMouseMove = dispatchWindowMouseEvent.bind(null, 'mousemove'); -const originalWindowScroll: Position = getWindowScrollPosition(); +const originalWindowScroll: Position = getWindowScroll(); type StartDrag = {| selection?: Position, From e3cce55cfcdc08d4e35f4e0c43fa85a5152438b7 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 15 Feb 2018 22:12:49 +1100 Subject: [PATCH 131/163] unconnected draggable tests --- test/unit/view/unconnected-draggable.spec.js | 57 +++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/test/unit/view/unconnected-draggable.spec.js b/test/unit/view/unconnected-draggable.spec.js index 0573d58467..451b8bcd81 100644 --- a/test/unit/view/unconnected-draggable.spec.js +++ b/test/unit/view/unconnected-draggable.spec.js @@ -123,6 +123,7 @@ const defaultMapProps: MapProps = { offset: origin, dimension: null, direction: null, + draggingOver: null, }; const somethingElseDraggingMapProps: MapProps = defaultMapProps; @@ -136,7 +137,7 @@ const draggingMapProps: MapProps = { // this may or may not be set during a drag dimension, direction: null, - + draggingOver: null, }; const dropAnimatingMapProps: MapProps = { @@ -147,6 +148,7 @@ const dropAnimatingMapProps: MapProps = { shouldAnimateDragMovement: false, dimension, direction: null, + draggingOver: null, }; const dropCompleteMapProps: MapProps = defaultMapProps; @@ -169,16 +171,18 @@ const mountDraggable = ({ // registering the droppable so that publishing the dimension will work correctly const dimensionMarshal: DimensionMarshal = createDimensionMarshal({ cancel: () => { }, - publishDraggables: () => { }, - publishDroppables: () => { }, + publishDraggable: () => { }, + publishDroppable: () => { }, updateDroppableScroll: () => { }, updateDroppableIsEnabled: () => { }, + bulkPublish: () => { }, }); dimensionMarshal.registerDroppable(droppable.descriptor, { getDimension: () => droppable, watchScroll: () => { }, unwatchScroll: () => { }, + scroll: () => {}, }); const wrapper: ReactWrapper = mount( @@ -228,12 +232,14 @@ const executeOnLift = (wrapper: ReactWrapper) => ({ selection = origin, center = origin, windowScroll = origin, - isScrollAllowed = false, }: StartDrag = {}) => { setWindowScroll(windowScroll); stubArea(center); - wrapper.find(DragHandle).props().callbacks.onLift({ client: selection, isScrollAllowed }); + wrapper.find(DragHandle).props().callbacks.onLift({ + client: selection, + autoScrollMode: 'FLUID', + }); }; // $ExpectError - not checking type of mock @@ -419,13 +425,12 @@ describe('Draggable - unconnected', () => { center, }; const windowScroll = { x: 100, y: 30 }; - const isScrollAllowed: boolean = true; - executeOnLift(wrapper)({ selection, center, windowScroll, isScrollAllowed }); + executeOnLift(wrapper)({ selection, center, windowScroll }); // $ExpectError - mock property on lift function expect(dispatchProps.lift.mock.calls[0]).toEqual([ - draggableId, initial, windowScroll, isScrollAllowed, + draggableId, initial, windowScroll, 'FLUID', ]); }); }); @@ -1016,6 +1021,42 @@ describe('Draggable - unconnected', () => { const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; expect(snapshot.isDragging).toBe(true); }); + + it('should let consumers know if draggging and over a droppable', () => { + // $ExpectError - using spread + const mapProps: MapProps = { + ...draggingMapProps, + draggingOver: 'foobar', + }; + + const myMock = jest.fn(); + + mountDraggable({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; + expect(snapshot.draggingOver).toBe('foobar'); + }); + + it('should let consumers know if dragging and not over a droppable', () => { + // $ExpectError - using spread + const mapProps: MapProps = { + ...draggingMapProps, + draggingOver: null, + }; + + const myMock = jest.fn(); + + mountDraggable({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; + expect(snapshot.draggingOver).toBe(null); + }); }); describe('drop animating', () => { From a154c720e55f5c0a67a5aa7346c5ac2e6235f090 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 15 Feb 2018 22:22:00 +1100 Subject: [PATCH 132/163] fixing connected draggable tests --- test/unit/view/connected-draggable.spec.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/unit/view/connected-draggable.spec.js b/test/unit/view/connected-draggable.spec.js index 00ed0fb995..3d5a8c0705 100644 --- a/test/unit/view/connected-draggable.spec.js +++ b/test/unit/view/connected-draggable.spec.js @@ -105,6 +105,7 @@ describe('Connected Draggable', () => { shouldAnimateDisplacement: false, dimension: preset.inHome1, direction: null, + draggingOver: null, }); }); @@ -210,6 +211,7 @@ describe('Connected Draggable', () => { // animation now controlled by isDropAnimating flag shouldAnimateDisplacement: false, shouldAnimateDragMovement: false, + draggingOver: null, }); }); }); @@ -236,6 +238,7 @@ describe('Connected Draggable', () => { // animation now controlled by isDropAnimating flag shouldAnimateDisplacement: false, shouldAnimateDragMovement: false, + draggingOver: null, }); }); }); @@ -453,6 +456,7 @@ describe('Connected Draggable', () => { shouldAnimateDragMovement: false, dimension: null, direction: null, + draggingOver: null, }); }); @@ -500,6 +504,7 @@ describe('Connected Draggable', () => { shouldAnimateDragMovement: false, dimension: null, direction: null, + draggingOver: null, }); }); @@ -586,6 +591,7 @@ describe('Connected Draggable', () => { shouldAnimateDragMovement: false, dimension: null, direction: null, + draggingOver: null, }); }); @@ -670,6 +676,7 @@ describe('Connected Draggable', () => { shouldAnimateDragMovement: false, dimension: null, direction: null, + draggingOver: null, }); }); @@ -755,6 +762,7 @@ describe('Connected Draggable', () => { shouldAnimateDragMovement: false, dimension: null, direction: null, + draggingOver: null, }); }); }); @@ -841,6 +849,7 @@ describe('Connected Draggable', () => { shouldAnimateDragMovement: false, dimension: null, direction: null, + draggingOver: null, }); }); }); @@ -889,10 +898,11 @@ describe('Connected Draggable', () => { // so that the draggable can publish itself const marshal: DimensionMarshal = createDimensionMarshal({ cancel: () => { }, - publishDraggables: () => { }, - publishDroppables: () => { }, + publishDraggable: () => { }, + publishDroppable: () => { }, updateDroppableScroll: () => { }, updateDroppableIsEnabled: () => { }, + bulkPublish: () => { }, }); const options: Object = combine( withStore(), @@ -908,6 +918,7 @@ describe('Connected Draggable', () => { getDimension: () => preset.home, watchScroll: () => { }, unwatchScroll: () => { }, + scroll: () => { }, }); class Person extends Component<{ name: string, provided: Provided}> { From 950aebfe02a306bc4c36593cf2d75614119a756e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 16 Feb 2018 09:06:46 +1100 Subject: [PATCH 133/163] more tests --- src/view/draggable/connected-draggable.js | 5 +- .../interactive-elements-app.jsx | 6 + test/unit/view/connected-draggable.spec.js | 395 ++++++++++++------ test/utils/dimension.js | 35 +- 4 files changed, 306 insertions(+), 135 deletions(-) diff --git a/src/view/draggable/connected-draggable.js b/src/view/draggable/connected-draggable.js index 67246e55e4..7830bb94dc 100644 --- a/src/view/draggable/connected-draggable.js +++ b/src/view/draggable/connected-draggable.js @@ -141,6 +141,8 @@ export const makeSelector = (): Selector => { const draggingOver: ?DroppableId = pending.result.destination ? pending.result.destination.droppableId : null; + const direction: ?Direction = pending.impact.direction ? + pending.impact.direction : null; // not memoized as it is the only execution return { @@ -150,8 +152,7 @@ export const makeSelector = (): Selector => { // still need to provide the dimension for the placeholder dimension: state.dimension.draggable[ownProps.draggableId], draggingOver, - // direction no longer needed as drag handle is unbound - direction: null, + direction, // animation will be controlled by the isDropAnimating flag shouldAnimateDragMovement: false, // not relevant, diff --git a/stories/src/interactive-elements/interactive-elements-app.jsx b/stories/src/interactive-elements/interactive-elements-app.jsx index 01c54aa788..77ec3cf2e7 100644 --- a/stories/src/interactive-elements/interactive-elements-app.jsx +++ b/stories/src/interactive-elements/interactive-elements-app.jsx @@ -46,6 +46,12 @@ const initial: ItemType[] = [
), }, + { + id: 'range', + component: ( + + ), + }, { id: 'content editable', component: ( diff --git a/test/unit/view/connected-draggable.spec.js b/test/unit/view/connected-draggable.spec.js index 3d5a8c0705..368024d564 100644 --- a/test/unit/view/connected-draggable.spec.js +++ b/test/unit/view/connected-draggable.spec.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { mount } from 'enzyme'; import Draggable, { makeSelector } from '../../../src/view/draggable/connected-draggable'; -import { getPreset } from '../../utils/dimension'; +import { getPreset, getInitialImpact, withImpact } from '../../utils/dimension'; import { negate } from '../../../src/state/position'; import createDimensionMarshal from '../../../src/state/dimension-marshal/dimension-marshal'; import getStatePreset from '../../utils/get-simple-state-preset'; @@ -29,6 +29,7 @@ import type { CurrentDragPositions, DragImpact, DraggableDimension, + DraggableLocation, } from '../../../src/types'; const preset = getPreset(); @@ -89,156 +90,304 @@ describe('Connected Draggable', () => { expect(console.error).toHaveBeenCalled(); }); - it('should move the dragging item to the current offset', () => { - const selector: Selector = makeSelector(); + describe('is not over a droppable', () => { + it('should move the dragging item to the current offset', () => { + const selector: Selector = makeSelector(); + + const result: MapProps = selector( + move(state.dragging(), { x: 20, y: 30 }), + ownProps + ); - const result: MapProps = selector( - move(state.dragging(), { x: 20, y: 30 }), - ownProps - ); - - expect(result).toEqual({ - isDropAnimating: false, - isDragging: true, - offset: { x: 20, y: 30 }, - shouldAnimateDragMovement: false, - shouldAnimateDisplacement: false, - dimension: preset.inHome1, - direction: null, - draggingOver: null, + expect(result).toEqual({ + isDropAnimating: false, + isDragging: true, + offset: { x: 20, y: 30 }, + shouldAnimateDragMovement: false, + shouldAnimateDisplacement: false, + dimension: preset.inHome1, + direction: null, + draggingOver: null, + }); }); - }); - it('should control whether drag movement is allowed based the current state', () => { - const selector: Selector = makeSelector(); - const previous: State = move(state.dragging(), { x: 20, y: 30 }); - - // drag animation is allowed - const allowed: State = { - ...previous, - drag: { - ...previous.drag, - current: { - // $ExpectError - not checking for null - ...previous.drag.current, - shouldAnimate: true, - }, - }, - }; - expect(selector(allowed, ownProps).shouldAnimateDragMovement).toBe(true); - - // drag animation is not allowed - const notAllowed: State = { - ...previous, - drag: { - ...previous.drag, - current: { - // $ExpectError - not checking for null - ...previous.drag.current, - shouldAnimate: false, + it('should control whether drag movement is allowed based the current state', () => { + const selector: Selector = makeSelector(); + const previous: State = move(state.dragging(), { x: 20, y: 30 }); + + // drag animation is allowed + const allowed: State = { + ...previous, + drag: { + ...previous.drag, + current: { + // $ExpectError - not checking for null + ...previous.drag.current, + shouldAnimate: true, + }, }, - }, - }; - expect(selector(notAllowed, ownProps).shouldAnimateDragMovement).toBe(false); - }); + }; + expect(selector(allowed, ownProps).shouldAnimateDragMovement).toBe(true); - it('should not break memoization on multiple calls to the same offset', () => { - const selector: Selector = makeSelector(); + // drag animation is not allowed + const notAllowed: State = { + ...previous, + drag: { + ...previous.drag, + current: { + // $ExpectError - not checking for null + ...previous.drag.current, + shouldAnimate: false, + }, + }, + }; + expect(selector(notAllowed, ownProps).shouldAnimateDragMovement).toBe(false); + }); - const result1: MapProps = selector( - move(state.dragging(), { x: 100, y: 200 }), - ownProps - ); - const result2: MapProps = selector( - move(state.dragging(), { x: 100, y: 200 }), - ownProps - ); - - expect(result1).toBe(result2); - expect(selector.recomputations()).toBe(1); - }); + it('should not break memoization on multiple calls to the same offset', () => { + const selector: Selector = makeSelector(); - it('should break memoization on multiple calls if moving to a new position', () => { - const selector: Selector = makeSelector(); + const result1: MapProps = selector( + move(state.dragging(), { x: 100, y: 200 }), + ownProps + ); + const result2: MapProps = selector( + move(state.dragging(), { x: 100, y: 200 }), + ownProps + ); - const result1: MapProps = selector( - move(state.dragging(), { x: 100, y: 200 }), - ownProps - ); - const result2: MapProps = selector( - move({ ...state.dragging() }, { x: 101, y: 200 }), - ownProps - ); - - expect(result1).not.toBe(result2); - expect(result1).not.toEqual(result2); - expect(selector.recomputations()).toBe(2); - }); + expect(result1).toBe(result2); + expect(selector.recomputations()).toBe(1); + }); - describe('drop animating', () => { - it('should log an error when there is invalid drag state', () => { - const invalid: State = { - ...state.dropAnimating(), - drop: null, - }; + it('should break memoization on multiple calls if moving to a new position', () => { const selector: Selector = makeSelector(); - const defaultMapProps: MapProps = selector(state.idle, ownProps); - const result: MapProps = selector(invalid, ownProps); + const result1: MapProps = selector( + move(state.dragging(), { x: 100, y: 200 }), + ownProps + ); + const result2: MapProps = selector( + move({ ...state.dragging() }, { x: 101, y: 200 }), + ownProps + ); - expect(result).toBe(defaultMapProps); - expect(console.error).toHaveBeenCalled(); + expect(result1).not.toBe(result2); + expect(result1).not.toEqual(result2); + expect(selector.recomputations()).toBe(2); }); - it('should move the draggable to the new offset', () => { + describe('drop animating', () => { + it('should log an error when there is invalid drag state', () => { + const invalid: State = { + ...state.dropAnimating(), + drop: null, + }; + const selector: Selector = makeSelector(); + const defaultMapProps: MapProps = selector(state.idle, ownProps); + + const result: MapProps = selector(invalid, ownProps); + + expect(result).toBe(defaultMapProps); + expect(console.error).toHaveBeenCalled(); + }); + + it('should move the draggable to the new offset', () => { + const selector: Selector = makeSelector(); + const current: State = state.dropAnimating(); + + const result: MapProps = selector( + current, + ownProps, + ); + + expect(result).toEqual({ + // no longer dragging + isDragging: false, + // is now drop animating + isDropAnimating: true, + // $ExpectError - not testing for null + offset: current.drop.pending.newHomeOffset, + dimension: preset.inHome1, + direction: null, + // animation now controlled by isDropAnimating flag + shouldAnimateDisplacement: false, + shouldAnimateDragMovement: false, + draggingOver: null, + }); + }); + }); + + describe('user cancel', () => { + it('should move the draggable to the new offset', () => { + const selector: Selector = makeSelector(); + const current: State = state.userCancel(); + + const result: MapProps = selector( + current, + ownProps, + ); + + expect(result).toEqual({ + // no longer dragging + isDragging: false, + // is now drop animating + isDropAnimating: true, + // $ExpectError - not testing for null + offset: current.drop.pending.newHomeOffset, + dimension: preset.inHome1, + direction: null, + // animation now controlled by isDropAnimating flag + shouldAnimateDisplacement: false, + shouldAnimateDragMovement: false, + draggingOver: null, + }); + }); + }); + }); + + describe('is over a droppable (test subset)', () => { + it('should move the dragging item to the current offset', () => { const selector: Selector = makeSelector(); - const current: State = state.dropAnimating(); const result: MapProps = selector( - current, - ownProps, + withImpact( + move(state.dragging(), { x: 20, y: 30 }), + getInitialImpact(preset.inHome1), + ), + ownProps ); expect(result).toEqual({ - // no longer dragging - isDragging: false, - // is now drop animating - isDropAnimating: true, - // $ExpectError - not testing for null - offset: current.drop.pending.newHomeOffset, - dimension: preset.inHome1, - direction: null, - // animation now controlled by isDropAnimating flag - shouldAnimateDisplacement: false, + isDropAnimating: false, + isDragging: true, + offset: { x: 20, y: 30 }, shouldAnimateDragMovement: false, - draggingOver: null, + shouldAnimateDisplacement: false, + dimension: preset.inHome1, + direction: preset.home.axis.direction, + draggingOver: preset.home.descriptor.id, }); }); - }); - describe('user cancel', () => { - it('should move the draggable to the new offset', () => { + it('should not break memoization on multiple calls to the same offset', () => { const selector: Selector = makeSelector(); - const current: State = state.userCancel(); - const result: MapProps = selector( - current, - ownProps, + const result1: MapProps = selector( + withImpact( + move(state.dragging(), { x: 100, y: 200 }), + getInitialImpact(preset.inHome1), + ), + ownProps + ); + const result2: MapProps = selector( + withImpact( + move(state.dragging(), { x: 100, y: 200 }), + getInitialImpact(preset.inHome1), + ), + ownProps ); - expect(result).toEqual({ - // no longer dragging - isDragging: false, - // is now drop animating - isDropAnimating: true, - // $ExpectError - not testing for null - offset: current.drop.pending.newHomeOffset, - dimension: preset.inHome1, - direction: null, - // animation now controlled by isDropAnimating flag - shouldAnimateDisplacement: false, - shouldAnimateDragMovement: false, - draggingOver: null, + expect(result1).toBe(result2); + expect(selector.recomputations()).toBe(1); + }); + + it('should break memoization on multiple calls if moving to a new position', () => { + const selector: Selector = makeSelector(); + + const result1: MapProps = selector( + withImpact( + move(state.dragging(), { x: 100, y: 200 }), + getInitialImpact(preset.inHome1) + ), + ownProps + ); + const result2: MapProps = selector( + withImpact( + move({ ...state.dragging() }, { x: 101, y: 200 }), + getInitialImpact(preset.inHome1), + ), + ownProps + ); + + expect(result1).not.toBe(result2); + expect(result1).not.toEqual(result2); + expect(selector.recomputations()).toBe(2); + }); + + describe('drop animating', () => { + it('should move the draggable to the new offset', () => { + const selector: Selector = makeSelector(); + const destination: DraggableLocation = { + index: preset.inHome1.descriptor.index, + droppableId: preset.inHome1.descriptor.droppableId, + }; + const current: State = withImpact( + state.dropAnimating(), + getInitialImpact(preset.inHome1) + ); + if (!current.drop || !current.drop.pending) { + throw new Error('invalid test setup'); + } + current.drop.pending.result.destination = destination; + + const result: MapProps = selector( + current, + ownProps, + ); + + expect(result).toEqual({ + // no longer dragging + isDragging: false, + // is now drop animating + isDropAnimating: true, + // $ExpectError - not testing for null + offset: current.drop.pending.newHomeOffset, + dimension: preset.inHome1, + direction: preset.home.axis.direction, + draggingOver: preset.home.descriptor.id, + // animation now controlled by isDropAnimating flag + shouldAnimateDisplacement: false, + shouldAnimateDragMovement: false, + }); + }); + }); + + describe('user cancel', () => { + it('should move the draggable to the new offset', () => { + const selector: Selector = makeSelector() + const destination: DraggableLocation = { + index: preset.inHome1.descriptor.index, + droppableId: preset.inHome1.descriptor.droppableId, + }; + const current: State = withImpact( + state.userCancel(), + getInitialImpact(preset.inHome1) + ); + if (!current.drop || !current.drop.pending) { + throw new Error('invalid test setup'); + } + current.drop.pending.result.destination = destination; + + const result: MapProps = selector( + current, + ownProps, + ); + + expect(result).toEqual({ + // no longer dragging + isDragging: false, + // is now drop animating + isDropAnimating: true, + // $ExpectError - not testing for null + offset: current.drop.pending.newHomeOffset, + dimension: preset.inHome1, + direction: preset.home.axis.direction, + draggingOver: preset.home.descriptor.id, + // animation now controlled by isDropAnimating flag + shouldAnimateDisplacement: false, + shouldAnimateDragMovement: false, + }); }); }); }); diff --git a/test/utils/dimension.js b/test/utils/dimension.js index e28d8b39f9..1d1689e99a 100644 --- a/test/utils/dimension.js +++ b/test/utils/dimension.js @@ -68,7 +68,7 @@ export const makeScrollable = (droppable: DroppableDimension, amount?: number = }); }; -export const getInitialImpact = (axis: Axis, draggable: DraggableDimension) => { +export const getInitialImpact = (draggable: DraggableDimension, axis?: Axis = vertical) => { const impact: DragImpact = { movement: noMovement, direction: axis.direction, @@ -81,16 +81,31 @@ export const getInitialImpact = (axis: Axis, draggable: DraggableDimension) => { }; export const withImpact = (state: State, impact: DragImpact) => { - if (!state.drag) { - throw new Error('invalid state'); + // while dragging + if (state.drag) { + return { + ...state, + drag: { + ...state.drag, + impact, + }, + }; } - return { - ...state, - drag: { - ...state.drag, - impact, - }, - }; + // while drop animating + if (state.drop && state.drop.pending) { + return { + ...state, + drop: { + ...state.drop, + pending: { + ...state.drop.pending, + impact, + }, + }, + }; + } + + throw new Error('unable to apply impact'); }; export const addDroppable = (base: State, droppable: DroppableDimension): State => ({ From e2ffd6b8823a865e7c4abb3d55c0bcc43eebf551 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 16 Feb 2018 09:40:46 +1100 Subject: [PATCH 134/163] updating connected droppable --- src/view/droppable/connected-droppable.js | 8 ++++---- test/unit/view/connected-droppable.spec.js | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index 08810b8f40..6c873b6aa7 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -98,13 +98,13 @@ export const makeSelector = (): Selector => { isDropDisabled: boolean, ): MapProps => { if (isDropDisabled) { - return getMapProps(false, null); + return getMapProps(false, null, null); } if (phase === 'DRAGGING') { if (!drag) { console.error('cannot determine dragging over as there is not drag'); - return getMapProps(false, null); + return getMapProps(false, null, null); } const isDraggingOver = getIsDraggingOver(id, drag.impact.destination); @@ -123,7 +123,7 @@ export const makeSelector = (): Selector => { if (phase === 'DROP_ANIMATING') { if (!pending) { console.error('cannot determine dragging over as there is no pending result'); - return getMapProps(false, null); + return getMapProps(false, null, null); } const isDraggingOver = getIsDraggingOver(id, pending.impact.destination); @@ -138,7 +138,7 @@ export const makeSelector = (): Selector => { return getMapProps(isDraggingOver, draggingOverWith, placeholder); } - return getMapProps(false, null); + return getMapProps(false, null, null); }, ); }; diff --git a/test/unit/view/connected-droppable.spec.js b/test/unit/view/connected-droppable.spec.js index 8652b78660..da04623065 100644 --- a/test/unit/view/connected-droppable.spec.js +++ b/test/unit/view/connected-droppable.spec.js @@ -2,7 +2,7 @@ /* eslint-disable react/no-multi-comp */ import React, { Component } from 'react'; import { mount } from 'enzyme'; -import { withStore, combine, withDimensionMarshal } from '../../utils/get-context-options'; +import { withStore, combine, withDimensionMarshal, withStyleContext } from '../../utils/get-context-options'; import Droppable, { makeSelector } from '../../../src/view/droppable/connected-droppable'; import getStatePreset from '../../utils/get-simple-state-preset'; import forceUpdate from '../../utils/force-update'; @@ -78,6 +78,7 @@ describe('Connected Droppable', () => { const selector: Selector = makeSelector(); const expected: MapProps = { isDraggingOver: false, + draggingOverWith: null, placeholder: null, }; @@ -91,6 +92,7 @@ describe('Connected Droppable', () => { const selector: Selector = makeSelector(); const expected: MapProps = { isDraggingOver: false, + draggingOverWith: null, placeholder: null, }; @@ -154,6 +156,7 @@ describe('Connected Droppable', () => { expect(result).toEqual({ isDraggingOver: true, + draggingOverWith: preset.inHome1.descriptor.id, placeholder: null, }); }); @@ -199,6 +202,7 @@ describe('Connected Droppable', () => { const selector: Selector = makeSelector(); const expected: MapProps = { isDraggingOver: true, + draggingOverWith: preset.inHome1.descriptor.id, placeholder: preset.inHome1.placeholder, }; @@ -266,7 +270,6 @@ describe('Connected Droppable', () => { drop: { result: null, pending: { - trigger: 'DROP', newHomeOffset: { x: 0, y: 0 }, impact, result: { @@ -280,6 +283,7 @@ describe('Connected Droppable', () => { index: preset.inHome1.descriptor.index, droppableId: preset.home.descriptor.id, }, + reason: 'DROP', }, }, }, @@ -343,7 +347,6 @@ describe('Connected Droppable', () => { drop: { result: null, pending: { - trigger: 'DROP', newHomeOffset: { x: 0, y: 0 }, impact, result: { @@ -354,6 +357,7 @@ describe('Connected Droppable', () => { droppableId: preset.home.descriptor.id, }, destination: impact.destination, + reason: 'DROP', }, }, }, @@ -390,7 +394,11 @@ describe('Connected Droppable', () => { }); describe('child render behavior', () => { - const contextOptions = combine(withStore(), withDimensionMarshal()); + const contextOptions = combine( + withStore(), + withDimensionMarshal(), + withStyleContext(), + ); class Person extends Component<{ name: string, provided: Provided }> { render() { From f8f23fca26c8aa9bc317cd469c51fe6ee7993c9c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 16 Feb 2018 14:31:55 +1100 Subject: [PATCH 135/163] more testing --- test/unit/view/connected-draggable.spec.js | 2 +- test/unit/view/drag-handle.spec.js | 154 +++++++++++-------- test/unit/view/unconnected-droppable.spec.js | 15 +- 3 files changed, 100 insertions(+), 71 deletions(-) diff --git a/test/unit/view/connected-draggable.spec.js b/test/unit/view/connected-draggable.spec.js index 368024d564..df521e5880 100644 --- a/test/unit/view/connected-draggable.spec.js +++ b/test/unit/view/connected-draggable.spec.js @@ -355,7 +355,7 @@ describe('Connected Draggable', () => { describe('user cancel', () => { it('should move the draggable to the new offset', () => { - const selector: Selector = makeSelector() + const selector: Selector = makeSelector(); const destination: DraggableLocation = { index: preset.inHome1.descriptor.index, droppableId: preset.inHome1.descriptor.droppableId, diff --git a/test/unit/view/drag-handle.spec.js b/test/unit/view/drag-handle.spec.js index c32078924c..44cb2538b9 100644 --- a/test/unit/view/drag-handle.spec.js +++ b/test/unit/view/drag-handle.spec.js @@ -220,6 +220,32 @@ describe('drag handle', () => { expect(myMock.mock.calls[0][0]['data-react-beautiful-dnd-drag-handle']).toEqual(basicContext[styleContextKey]); }); + it('should apply a default aria roledescription containing lift instructions', () => { + const myMock = jest.fn(); + myMock.mockReturnValue(
hello world
); + + mount( + fakeDraggableRef} + canDragInteractiveElements={false} + > + {(dragHandleProps: ?DragHandleProps) => ( + myMock(dragHandleProps) + )} + , + { context: basicContext } + ); + + // $ExpectError - using lots of accessors + expect(myMock.mock.calls[0][0]['aria-roledescription']) + .toBe('Draggable item. Press space bar to lift'); + }); + describe('mouse dragging', () => { describe('initiation', () => { it('should start a drag if there was sufficient mouse movement in any direction', () => { @@ -253,7 +279,7 @@ describe('drag handle', () => { windowMouseMove(point); expect(customCallbacks.onLift) - .toHaveBeenCalledWith({ client: point, isScrollAllowed: true }); + .toHaveBeenCalledWith({ client: point, autoScrollMode: 'FLUID' }); customWrapper.unmount(); }); @@ -1166,7 +1192,7 @@ describe('drag handle', () => { expect(callbacks.onLift).toHaveBeenCalledWith({ client: fakeCenter, - isScrollAllowed: false, + autoScrollMode: 'JUMP', }); }); @@ -1263,6 +1289,39 @@ describe('drag handle', () => { })).toBe(true); }); + it('should instantly fire a scroll action when the window scrolls', () => { + // lift + pressSpacebar(wrapper); + // scroll event + window.dispatchEvent(new Event('scroll')); + + expect(callbacksCalled(callbacks)({ + onLift: 1, + onWindowScroll: 1, + })).toBe(true); + }); + + it.only('should prevent using keyboard keys that modify scroll', () => { + const keys: number[] = [ + keyCodes.pageUp, + keyCodes.pageDown, + keyCodes.home, + keyCodes.end, + ]; + + // lift + pressSpacebar(wrapper); + + keys.forEach((keyCode: number) => { + const trigger = dispatchWindowKeyDownEvent.bind(null, keyCode); + + const event: KeyboardEvent = trigger(); + + expect(event.defaultPrevented).toBe(true); + expect(callbacks.onWindowScroll).not.toHaveBeenCalled(); + }); + }); + it('should stop dragging if the keyboard is used after a lift and a direction is not provided', () => { const customCallbacks = getStubCallbacks(); const customWrapper = mount( @@ -1557,28 +1616,8 @@ describe('drag handle', () => { }); }); - it('should cancel when the window is resized', () => { - // lift - pressSpacebar(wrapper); - // resize event - window.dispatchEvent(new Event('resize')); - - expect(callbacksCalled(callbacks)({ - onLift: 1, - onCancel: 1, - })).toBe(true); - }); - - it('should cancel if the window is scrolled', () => { - // lift - pressSpacebar(wrapper); - // scroll event - window.dispatchEvent(new Event('scroll')); - - expect(callbacksCalled(callbacks)({ - onLift: 1, - onCancel: 1, - })).toBe(true); + it.skip('should cancel on a page visibility change', () => { + // TODO }); it('should not do anything if there is nothing dragging', () => { @@ -1737,7 +1776,10 @@ describe('drag handle', () => { touchStart(wrapper, client); jest.runTimersToTime(timeForLongPress); - expect(callbacks.onLift).toHaveBeenCalledWith({ client, isScrollAllowed: false }); + expect(callbacks.onLift).toHaveBeenCalledWith({ + client, + autoScrollMode: 'FLUID', + }); }); it('should not fire a second lift after movement that would have otherwise have started a drag', () => { @@ -1935,6 +1977,25 @@ describe('drag handle', () => { expect(event.defaultPrevented).toBe(true); }); + + it('should schedule a window scroll move on window scroll', () => { + start(); + + dispatchWindowEvent('scroll'); + dispatchWindowEvent('scroll'); + dispatchWindowEvent('scroll'); + + // not called initially + expect(callbacks.onWindowScroll).not.toHaveBeenCalled(); + + // called after a requestAnimationFrame + requestAnimationFrame.step(); + expect(callbacks.onWindowScroll).toHaveBeenCalledTimes(1); + + // should not add any additional calls + requestAnimationFrame.flush(); + expect(callbacks.onWindowScroll).toHaveBeenCalledTimes(1); + }); }); describe('dropping', () => { @@ -2052,15 +2113,6 @@ describe('drag handle', () => { })).toBe(true); }); - it('should cancel the drag after a window scroll', () => { - dispatchWindowEvent('scroll'); - - expect(callbacksCalled(callbacks)({ - onLift: 1, - onCancel: 1, - })).toBe(true); - }); - it('should cancel a drag if any keypress is made', () => { // end initial drag end(); @@ -2366,9 +2418,6 @@ describe('drag handle', () => { const controls: Control[] = [mouse, keyboard, touch]; - const getAria = (wrap?: ReactWrapper = wrapper): boolean => - Boolean(wrap.find(Child).props().dragHandleProps['aria-grabbed']); - beforeEach(() => { jest.useFakeTimers(); }); @@ -2379,37 +2428,6 @@ describe('drag handle', () => { controls.forEach((control: Control) => { describe(`control: ${control.name}`, () => { - describe('aria', () => { - it('should not set the aria attribute of dragging if not dragging', () => { - expect(getAria()).toBe(false); - }); - - it('should not set the aria attribute of dragging if a drag is pending', () => { - control.preLift(); - forceUpdate(wrapper); - - expect(getAria()).toBe(false); - }); - - it('should set the aria attribute of dragging if a drag is occurring', () => { - control.preLift(); - control.lift(); - forceUpdate(wrapper); - - expect(getAria()).toBe(true); - }); - - it('should set the aria attribute if drag is finished', () => { - control.preLift(); - control.lift(); - forceUpdate(wrapper); - control.end(); - forceUpdate(wrapper); - - expect(getAria()).toBe(false); - }); - }); - describe('window bindings', () => { it('should unbind all window listeners when drag ends', () => { jest.spyOn(window, 'addEventListener'); diff --git a/test/unit/view/unconnected-droppable.spec.js b/test/unit/view/unconnected-droppable.spec.js index 49551b1978..c0e05df8a8 100644 --- a/test/unit/view/unconnected-droppable.spec.js +++ b/test/unit/view/unconnected-droppable.spec.js @@ -5,7 +5,7 @@ import { mount } from 'enzyme'; import type { ReactWrapper } from 'enzyme'; import Droppable from '../../../src/view/droppable/droppable'; import Placeholder from '../../../src/view/placeholder/'; -import { withStore, combine, withDimensionMarshal } from '../../utils/get-context-options'; +import { withStore, combine, withDimensionMarshal, withStyleContext } from '../../utils/get-context-options'; import { getPreset } from '../../utils/dimension'; import type { DroppableId, DraggableDimension } from '../../../src/types'; import type { MapProps, OwnProps, Provided, StateSnapshot } from '../../../src/view/droppable/droppable-types'; @@ -23,12 +23,15 @@ const getStubber = (mock: Function) => } }; const defaultDroppableId: DroppableId = 'droppable-1'; +const draggableId: DraggableId = 'draggable-1'; const notDraggingOverMapProps: MapProps = { isDraggingOver: false, + draggingOverWith: null, placeholder: null, }; const isDraggingOverHomeMapProps: MapProps = { isDraggingOver: true, + draggingOverWith: draggableId, placeholder: null, }; @@ -37,6 +40,7 @@ const inHome1: DraggableDimension = data.inHome1; const isDraggingOverForeignMapProps: MapProps = { isDraggingOver: true, + draggingOverWith: 'draggable-1', placeholder: inHome1.placeholder, }; @@ -66,7 +70,11 @@ const mountDroppable = ({ )} , - combine(withStore(), withDimensionMarshal()) + combine( + withStore(), + withDimensionMarshal(), + withStyleContext(), + ) ); describe('Droppable - unconnected', () => { @@ -82,6 +90,7 @@ describe('Droppable - unconnected', () => { const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot; expect(provided.innerRef).toBeInstanceOf(Function); expect(snapshot.isDraggingOver).toBe(true); + expect(snapshot.draggingOverWith).toBe(draggableId); expect(provided.placeholder).toBe(null); }); }); @@ -98,6 +107,7 @@ describe('Droppable - unconnected', () => { const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot; expect(provided.innerRef).toBeInstanceOf(Function); expect(snapshot.isDraggingOver).toBe(true); + expect(snapshot.draggingOverWith).toBe(draggableId); // $ExpectError - type property of placeholder expect(provided.placeholder.type).toBe(Placeholder); // $ExpectError - props property of placeholder @@ -117,6 +127,7 @@ describe('Droppable - unconnected', () => { const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot; expect(provided.innerRef).toBeInstanceOf(Function); expect(snapshot.isDraggingOver).toBe(false); + expect(snapshot.draggingOverWith).toBe(null); expect(provided.placeholder).toBe(null); }); }); From ceb00653c697bb07fbb0f8172b2e4c124d33643f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Sun, 18 Feb 2018 14:03:17 +1100 Subject: [PATCH 136/163] moving from index to map for interactive element lookup --- .../util/should-allow-dragging-from-target.js | 27 ++++++++++--------- test/unit/view/drag-handle.spec.js | 26 ++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/view/drag-handle/util/should-allow-dragging-from-target.js b/src/view/drag-handle/util/should-allow-dragging-from-target.js index 1affdd9de1..58a40c0077 100644 --- a/src/view/drag-handle/util/should-allow-dragging-from-target.js +++ b/src/view/drag-handle/util/should-allow-dragging-from-target.js @@ -1,16 +1,20 @@ // @flow import type { Props } from '../drag-handle-types'; -export const interactiveTagNames: string[] = [ - 'input', - 'button', - 'textarea', - 'select', - 'option', - 'optgroup', - 'video', - 'audio', -]; +export type TagNameMap = { + [tagName: string]: true +} + +export const interactiveTagNames: TagNameMap = { + input: true, + button: true, + textarea: true, + select: true, + option: true, + optgroup: true, + video: true, + audio: true, +}; const isContentEditable = (parent: Element, current: ?Element): boolean => { if (current == null) { @@ -48,8 +52,7 @@ export default (event: Event, props: Props): boolean => { return true; } - const isTargetInteractive: boolean = - interactiveTagNames.indexOf(target.tagName.toLowerCase()) !== -1; + const isTargetInteractive: boolean = Boolean(interactiveTagNames[target.tagName.toLowerCase()]); if (isTargetInteractive) { return false; diff --git a/test/unit/view/drag-handle.spec.js b/test/unit/view/drag-handle.spec.js index 44cb2538b9..5eaa5215ad 100644 --- a/test/unit/view/drag-handle.spec.js +++ b/test/unit/view/drag-handle.spec.js @@ -20,10 +20,10 @@ import type { Position, DraggableId } from '../../../src/types'; import * as keyCodes from '../../../src/view/key-codes'; import getWindowScroll from '../../../src/window/get-window-scroll'; import setWindowScroll from '../../utils/set-window-scroll'; -import forceUpdate from '../../utils/force-update'; import getArea from '../../../src/state/get-area'; import { timeForLongPress, forcePressThreshold } from '../../../src/view/drag-handle/sensor/create-touch-sensor'; import { interactiveTagNames } from '../../../src/view/drag-handle/util/should-allow-dragging-from-target'; +import type { TagNameMap } from '../../../src/view/drag-handle/util/should-allow-dragging-from-target'; import { styleContextKey, canLiftContextKey } from '../../../src/view/context-keys'; const primaryButton: number = 0; @@ -1301,7 +1301,7 @@ describe('drag handle', () => { })).toBe(true); }); - it.only('should prevent using keyboard keys that modify scroll', () => { + it('should prevent using keyboard keys that modify scroll', () => { const keys: number[] = [ keyCodes.pageUp, keyCodes.pageDown, @@ -1313,11 +1313,12 @@ describe('drag handle', () => { pressSpacebar(wrapper); keys.forEach((keyCode: number) => { - const trigger = dispatchWindowKeyDownEvent.bind(null, keyCode); + const mockEvent: MockEvent = createMockEvent(); + const trigger = withKeyboard(keyCode); - const event: KeyboardEvent = trigger(); + trigger(wrapper, mockEvent); - expect(event.defaultPrevented).toBe(true); + expect(wasEventStopped(mockEvent)).toBe(true); expect(callbacks.onWindowScroll).not.toHaveBeenCalled(); }); }); @@ -2459,9 +2460,9 @@ describe('drag handle', () => { }); describe('interactive element interactions', () => { - const mixedCase = (items: string[]): string[] => [ - ...items.map((i: string): string => i.toLowerCase()), - ...items.map((i: string): string => i.toUpperCase()), + const mixedCase = (map: TagNameMap): string[] => [ + ...Object.keys(map).map((tagName: string) => tagName.toLowerCase()), + ...Object.keys(map).map((tagName: string) => tagName.toUpperCase()), ]; it('should not start a drag if the target is an interactive element', () => { @@ -2502,9 +2503,12 @@ describe('drag handle', () => { }); it('should start a drag if the target is not an interactive element', () => { - const nonInteractiveTagNames: string[] = [ - 'a', 'div', 'span', 'header', - ]; + const nonInteractiveTagNames: TagNameMap = { + a: true, + div: true, + span: true, + header: true, + }; // counting call count between loops let count: number = 0; From 80741cfdd4c0b281bc3b70cb697879ffd7367c2c Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Sun, 18 Feb 2018 21:58:46 +1100 Subject: [PATCH 137/163] new forced visibility displacement --- .../get-forced-displaced.js | 124 ++++++++++++++++++ .../move-to-next-index/in-foreign-list.js | 74 ++++------- src/state/move-to-next-index/in-home-list.js | 53 +++----- test/unit/state/move-to-next-index.spec.js | 16 +++ 4 files changed, 188 insertions(+), 79 deletions(-) create mode 100644 src/state/move-to-next-index/get-forced-displaced.js diff --git a/src/state/move-to-next-index/get-forced-displaced.js b/src/state/move-to-next-index/get-forced-displaced.js new file mode 100644 index 0000000000..815d89e6ec --- /dev/null +++ b/src/state/move-to-next-index/get-forced-displaced.js @@ -0,0 +1,124 @@ +// @flow +import getDisplacement from '../get-displacement'; +import type { + Area, + Axis, + Position, + DraggableId, + DragImpact, + DraggableDimensionMap, + DroppableDimension, + DraggableDimension, + Displacement, +} from '../../types'; + +type WithAdded = {| + add: DraggableId, + previousImpact: DragImpact, + droppable: DroppableDimension, + draggables: DraggableDimensionMap, + viewport: Area, +|} + +export const withFirstAdded = ({ + add, + previousImpact, + droppable, + draggables, + viewport, +}: WithAdded): Displacement[] => { + const newDisplacement: Displacement = { + draggableId: add, + isVisible: true, + shouldAnimate: true, + }; + + const added: Displacement[] = [ + newDisplacement, + ...previousImpact.movement.displaced, + ]; + + const withUpdatedVisibility: Displacement[] = + added.map((current: Displacement): Displacement => { + // we have already calculated the displacement for this item + if (current === newDisplacement) { + return current; + } + + const updated: Displacement = getDisplacement({ + draggable: draggables[current.draggableId], + destination: droppable, + previousImpact, + viewport, + }); + + return updated; + }); + + return withUpdatedVisibility; +}; + +type WithLastRemoved = {| + distanceMoving: Position, + previousImpact: DragImpact, + droppable: DroppableDimension, + draggables: DraggableDimensionMap, + viewport: Area, +|} + +export const withFirstRemoved = ({ + distanceMoving, + previousImpact, + droppable, + draggables, + viewport, +}: WithLastRemoved): Displacement[] => { + const last: Displacement[] = previousImpact.movement.displaced; + if (!last.length) { + console.error('cannot remove displacement from empty list'); + return []; + } + + const removed: Displacement[] = last.slice(1, last.length); + + const axis: Axis = droppable.axis; + let buffer: number = distanceMoving[axis.line]; + + const withUpdatedVisibility: Displacement[] = + removed.map((displacement: Displacement): Displacement => { + // we need to ensure that the previous items up to the size of the + // dragging item has a visible movement. This is because a movement + // can result in a combination of scrolls that have this effect + if (buffer > 0) { + const current: DraggableDimension = draggables[displacement.draggableId]; + const size: number = current.page.withMargin[axis.size]; + buffer -= size; + + // displacement was already visible - can leave it unmodified + if (displacement.isVisible) { + return displacement; + } + + // the displacement was not visible - we need to force it to be visible and in + // place immediately. + return { + draggableId: displacement.draggableId, + isVisible: true, + shouldAnimate: false, + }; + } + + // We are outside of the buffer - we can now execute standard visibility checks + const updated: Displacement = getDisplacement({ + draggable: draggables[displacement.draggableId], + destination: droppable, + previousImpact, + viewport, + }); + + return updated; + }); + + return withUpdatedVisibility; +}; + diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index 745a8c3855..9674465bc1 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -1,11 +1,11 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch, subtract } from '../position'; +import { patch, subtract, absolute } from '../position'; import withDroppableDisplacement from '../with-droppable-displacement'; import moveToEdge from '../move-to-edge'; -import getDisplacement from '../get-displacement'; import getViewport from '../../window/get-viewport'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; +import { withFirstAdded, withFirstRemoved } from './get-forced-displaced'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import type { @@ -82,53 +82,32 @@ export default ({ destinationEdge, destinationAxis: droppable.axis, }); + // The full distance required to get from the previous page center to the new page center + const distanceMoving: Position = subtract(newPageCenter, previousPageCenter); + const distanceWithScroll: Position = withDroppableDisplacement(droppable, distanceMoving); - const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newPageCenter, - viewport, - }); - - // at this point we know that the destination is droppable - const movingRelativeToDisplacement: Displacement = { - draggableId: movingRelativeTo.descriptor.id, - isVisible: true, - shouldAnimate: true, - }; - - // When we are in foreign list we are only displacing items forward - // This list is always sorted by the closest impacted draggable - const modified: Displacement[] = (isMovingForward ? - // Stop displacing the closest draggable forward - previousImpact.movement.displaced.slice(1, previousImpact.movement.displaced.length) : - // Add the draggable that we are moving into the place of - [movingRelativeToDisplacement, ...previousImpact.movement.displaced]); - - // update displacement to consider viewport and droppable visibility - const displaced: Displacement[] = modified - .map((displacement: Displacement): Displacement => { - // already processed - if (displacement === movingRelativeToDisplacement) { - return displacement; - } - - const target: DraggableDimension = draggables[displacement.draggableId]; - - const updated: Displacement = getDisplacement({ - draggable: target, - destination: droppable, - viewport, + const displaced: Displacement[] = (() => { + if (isMovingForward) { + return withFirstRemoved({ + distanceMoving: absolute(distanceWithScroll), previousImpact, + droppable, + draggables, + viewport, }); - - return updated; + } + return withFirstAdded({ + add: movingRelativeTo.descriptor.id, + previousImpact, + droppable, + draggables, + viewport, }); + })(); const newImpact: DragImpact = { movement: { displaced, - // The amount of movement will always be the size of the dragging item amount: patch(axis.line, draggable.page.withMargin[axis.size]), // When we are in foreign list we are only displacing items forward isBeyondStartPosition: false, @@ -140,6 +119,13 @@ export default ({ direction: droppable.axis.direction, }; + const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ + draggable, + destination: droppable, + newPageCenter, + viewport, + }); + if (isVisibleInNewLocation) { return { pageCenter: withDroppableDisplacement(droppable, newPageCenter), @@ -148,13 +134,9 @@ export default ({ }; } - // The full distance required to get from the previous page center to the new page center - const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); - const requiredScroll: Position = withDroppableDisplacement(droppable, requiredDistance); - return { pageCenter: previousPageCenter, impact: newImpact, - scrollJumpRequest: requiredScroll, + scrollJumpRequest: distanceWithScroll, }; }; diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index d90de8276a..2a6c10e7d1 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,12 +1,12 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch, subtract, negate } from '../position'; +import { patch, subtract, absolute } from '../position'; import withDroppableDisplacement from '../with-droppable-displacement'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import getViewport from '../../window/get-viewport'; // import getScrollJumpResult from './get-scroll-jump-result'; import moveToEdge from '../move-to-edge'; -import getDisplacement from '../get-displacement'; +import { withFirstAdded, withFirstRemoved } from './get-forced-displaced'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import type { @@ -83,41 +83,32 @@ export default ({ destinationAxis: droppable.axis, }); - // As this is a forced displacement we always want it to be visible and animate - const destinationDisplacement: Displacement = { - draggableId: destination.descriptor.id, - isVisible: true, - shouldAnimate: true, - }; + // The full distance required to get from the previous page center to the new page center + const distance: Position = subtract(newPageCenter, previousPageCenter); + const distanceWithScroll: Position = withDroppableDisplacement(droppable, distance); - const modified: Displacement[] = (isMovingTowardStart ? - // remove the most recently impacted - previousImpact.movement.displaced.slice(1, previousImpact.movement.displaced.length) : - // add the destination as the most recently impacted - [destinationDisplacement, ...previousImpact.movement.displaced]); - - // update impact with visibility - stops redundant work! - const displaced: Displacement[] = modified - .map((displacement: Displacement): Displacement => { - // we have already calculated the displacement for this item - if (displacement === destinationDisplacement) { - return displacement; - } - - const updated: Displacement = getDisplacement({ - draggable: draggables[displacement.draggableId], - destination: droppable, + const displaced: Displacement[] = (() => { + if (isMovingTowardStart) { + return withFirstRemoved({ + distanceMoving: absolute(distanceWithScroll), previousImpact, + droppable, + draggables, viewport, }); - - return updated; + } + return withFirstAdded({ + add: destination.descriptor.id, + previousImpact, + droppable, + draggables, + viewport, }); + })(); const newImpact: DragImpact = { movement: { displaced, - // The amount of movement will always be the size of the dragging item amount: patch(axis.line, draggable.page.withMargin[axis.size]), isBeyondStartPosition: proposedIndex > startIndex, }, @@ -143,13 +134,9 @@ export default ({ }; } - // The full distance required to get from the previous page center to the new page center - const requiredDistance: Position = subtract(newPageCenter, previousPageCenter); - const requiredScroll: Position = withDroppableDisplacement(droppable, requiredDistance); - return { pageCenter: previousPageCenter, impact: newImpact, - scrollJumpRequest: requiredScroll, + scrollJumpRequest: distanceWithScroll, }; }; diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index 9e64d3db0f..301efb9f2c 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -19,6 +19,7 @@ import type { DroppableDimension, DraggableLocation, Position, + Displacement, } from '../../../src/types'; const setViewport = (custom: Area): void => { @@ -439,6 +440,21 @@ describe('move to next index', () => { expect(result.impact).toEqual(expected); }); }); + + describe('forced visibility displacement', () => { + // TODO >< + it('should force the displacement of the closest item to be visible', () => { + + }); + + it('should use a previous displacement if it was visible', () => { + + }); + + it('should consider any change in the droppables scroll', () => { + + }); + }); }); }); From 2b7cffed3838331dcb1a0e02aa3d9cca5ba96919 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 19 Feb 2018 08:08:56 +1100 Subject: [PATCH 138/163] force push progress --- .../get-forced-displaced.js | 6 ++-- .../move-to-next-index/in-foreign-list.js | 2 +- src/state/move-to-next-index/in-home-list.js | 2 +- test/unit/state/move-to-next-index.spec.js | 29 +++++++++---------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/state/move-to-next-index/get-forced-displaced.js b/src/state/move-to-next-index/get-forced-displaced.js index 815d89e6ec..2b96578fb7 100644 --- a/src/state/move-to-next-index/get-forced-displaced.js +++ b/src/state/move-to-next-index/get-forced-displaced.js @@ -82,13 +82,15 @@ export const withFirstRemoved = ({ const removed: Displacement[] = last.slice(1, last.length); const axis: Axis = droppable.axis; - let buffer: number = distanceMoving[axis.line]; + let buffer: number = Math.abs(distanceMoving[axis.line]); const withUpdatedVisibility: Displacement[] = removed.map((displacement: Displacement): Displacement => { // we need to ensure that the previous items up to the size of the // dragging item has a visible movement. This is because a movement - // can result in a combination of scrolls that have this effect + // can result in a combination of scrolls that have this effect. + // Technicall we could just do this when the destination is not visible - but + // there is no harm doing it all the time for simplicity if (buffer > 0) { const current: DraggableDimension = draggables[displacement.draggableId]; const size: number = current.page.withMargin[axis.size]; diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index 9674465bc1..3825e7f690 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -89,7 +89,7 @@ export default ({ const displaced: Displacement[] = (() => { if (isMovingForward) { return withFirstRemoved({ - distanceMoving: absolute(distanceWithScroll), + distanceMoving: distanceWithScroll, previousImpact, droppable, draggables, diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 2a6c10e7d1..8cb52f9058 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -90,7 +90,7 @@ export default ({ const displaced: Displacement[] = (() => { if (isMovingTowardStart) { return withFirstRemoved({ - distanceMoving: absolute(distanceWithScroll), + distanceMoving: distanceWithScroll, previousImpact, droppable, draggables, diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index 301efb9f2c..f73d1ad2ba 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -440,21 +440,6 @@ describe('move to next index', () => { expect(result.impact).toEqual(expected); }); }); - - describe('forced visibility displacement', () => { - // TODO >< - it('should force the displacement of the closest item to be visible', () => { - - }); - - it('should use a previous displacement if it was visible', () => { - - }); - - it('should consider any change in the droppables scroll', () => { - - }); - }); }); }); @@ -1469,6 +1454,20 @@ describe('move to next index', () => { }); }); }); + + describe('forced visibility displacement', () => { + it('should force the displacement of the closest item to be visible', () => { + + }); + + it('should use a previous displacement if it was visible', () => { + + }); + + it('should consider any change in the droppables scroll', () => { + + }); + }); }); }); }); From 6d1a9e900e5900e85b4b29e65a916f149607d726 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 19 Feb 2018 13:40:47 +1100 Subject: [PATCH 139/163] initial --- src/state/get-displacement.js | 3 +- .../get-forced-displaced.js | 28 +- test/unit/state/move-to-next-index.spec.js | 258 ++++++++++++++++-- 3 files changed, 269 insertions(+), 20 deletions(-) diff --git a/src/state/get-displacement.js b/src/state/get-displacement.js index 122eb140d6..04d49cb1a9 100644 --- a/src/state/get-displacement.js +++ b/src/state/get-displacement.js @@ -17,7 +17,8 @@ type Args = {| viewport: Area, |} -// Note: it is also an optimisation to undo the displacement on items when they are no longer visible. +// Note: it is also an optimisation to undo the displacement on +// items when they are no longer visible. // This prevents a lot of .render() calls when leaving a list export default ({ diff --git a/src/state/move-to-next-index/get-forced-displaced.js b/src/state/move-to-next-index/get-forced-displaced.js index 2b96578fb7..7c42873ac5 100644 --- a/src/state/move-to-next-index/get-forced-displaced.js +++ b/src/state/move-to-next-index/get-forced-displaced.js @@ -84,25 +84,38 @@ export const withFirstRemoved = ({ const axis: Axis = droppable.axis; let buffer: number = Math.abs(distanceMoving[axis.line]); + console.group('forced'); + console.log('starting buffer', buffer); + const withUpdatedVisibility: Displacement[] = removed.map((displacement: Displacement): Displacement => { // we need to ensure that the previous items up to the size of the // dragging item has a visible movement. This is because a movement // can result in a combination of scrolls that have this effect. - // Technicall we could just do this when the destination is not visible - but + // Technically we could just do this when the destination is not visible - but // there is no harm doing it all the time for simplicity - if (buffer > 0) { + + // It seems to provide a more accurate experience to force displacement when buffer >= 0 + // rather than just > 0. + console.log('buffer', buffer); + if (buffer >= 0) { const current: DraggableDimension = draggables[displacement.draggableId]; - const size: number = current.page.withMargin[axis.size]; + + // Using the 'withoutMargin' size. When moving often we do not consider the margin + // as a part of the calculations for determining the new center position. As such, + // we do not reduce the buffer by the size of the margin. + const size: number = current.page.withoutMargin[axis.size]; buffer -= size; // displacement was already visible - can leave it unmodified if (displacement.isVisible) { + console.log('returning original for', displacement.draggableId); return displacement; } // the displacement was not visible - we need to force it to be visible and in // place immediately. + console.log('forcing for', displacement.draggableId); return { draggableId: displacement.draggableId, isVisible: true, @@ -110,6 +123,13 @@ export const withFirstRemoved = ({ }; } + console.log('recalculating for', displacement.draggableId, getDisplacement({ + draggable: draggables[displacement.draggableId], + destination: droppable, + previousImpact, + viewport, + })); + // We are outside of the buffer - we can now execute standard visibility checks const updated: Displacement = getDisplacement({ draggable: draggables[displacement.draggableId], @@ -121,6 +141,8 @@ export const withFirstRemoved = ({ return updated; }); + console.groupEnd(); + return withUpdatedVisibility; }; diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index f73d1ad2ba..efe5a7abc7 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -1,15 +1,17 @@ // @flow import moveToNextIndex from '../../../src/state/move-to-next-index/'; import type { Result } from '../../../src/state/move-to-next-index/move-to-next-index-types'; -import { getPreset, disableDroppable } from '../../utils/dimension'; +import { getPreset, disableDroppable, getClosestScrollable } from '../../utils/dimension'; import moveToEdge from '../../../src/state/move-to-edge'; import noImpact, { noMovement } from '../../../src/state/no-impact'; import { patch, subtract } from '../../../src/state/position'; import { vertical, horizontal } from '../../../src/state/axis'; +import { isPartiallyVisible } from '../../../src/state/visibility/is-visible'; import getViewport from '../../../src/window/get-viewport'; import getArea from '../../../src/state/get-area'; import setWindowScroll from '../../utils/set-window-scroll'; -import { getDroppableDimension, getDraggableDimension } from '../../../src/state/dimension'; +import { getDroppableDimension, getDraggableDimension, scrollDroppable } from '../../../src/state/dimension'; +import getMaxScroll from '../../../src/state/get-max-scroll'; import type { Area, Axis, @@ -440,6 +442,244 @@ describe('move to next index', () => { expect(result.impact).toEqual(expected); }); }); + + describe('forced visibility displacement', () => { + const crossAxisStart: number = 0; + const crossAxisEnd: number = 100; + + const droppableScrollSize = { + scrollHeight: axis === vertical ? 350 : crossAxisEnd, + scrollWidth: axis === horizontal ? 350 : crossAxisEnd, + }; + + const home: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'home', + type: 'TYPE', + }, + direction: axis.direction, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + [axis.end]: 350, + }), + closest: { + frameClient: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + // will cut off the subject + [axis.end]: 100, + }), + scrollHeight: droppableScrollSize.scrollHeight, + scrollWidth: droppableScrollSize.scrollWidth, + shouldClipSubject: true, + scroll: { x: 0, y: 0 }, + }, + }); + + const maxScroll: Position = getClosestScrollable(home).scroll.max; + + // half the size of the viewport + const inHome1: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inHome1', + droppableId: home.descriptor.id, + index: 0, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + [axis.end]: 50, + }), + }); + + const inHome2: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inHome2', + droppableId: home.descriptor.id, + index: 1, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 50, + [axis.end]: 100, + }), + }); + + // half + const inHome3: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inHome3', + droppableId: home.descriptor.id, + index: 2, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 100, + [axis.end]: 150, + }), + }); + + // half as big as the frame (50px) + const inHome4: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inHome4', + droppableId: home.descriptor.id, + index: 3, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 200, + [axis.end]: 250, + }), + }); + + // half as big as the frame (50px) + const inHome5: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inHome5', + droppableId: home.descriptor.id, + index: 4, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 300, + [axis.end]: 350, + }), + }); + + const draggables: DraggableDimensionMap = { + [inHome1.descriptor.id]: inHome1, + [inHome2.descriptor.id]: inHome2, + [inHome3.descriptor.id]: inHome3, + [inHome4.descriptor.id]: inHome4, + [inHome5.descriptor.id]: inHome5, + }; + + it('should force the displacement of the items up to the distance of the movement', () => { + // We have moved inHome1 to the end of the list + const previousImpact: DragImpact = { + movement: { + // ordered by most recently impacted + displaced: [ + // the last impact would have been before the last addition. + // At this point the last two items would have been visible + { + draggableId: inHome5.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: inHome4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: inHome3.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, + { + draggableId: inHome2.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, + ], + amount: patch(axis.line, inHome1.page.withMargin[axis.size]), + isBeyondStartPosition: true, + }, + direction: axis.direction, + // is now in the last position + destination: { + droppableId: home.descriptor.id, + index: 4, + }, + }; + // home has now scrolled to the bottom + const scrolled: DroppableDimension = scrollDroppable(home, maxScroll); + + // validation of previous impact + expect(isPartiallyVisible({ + target: inHome5.page.withMargin, + destination: scrolled, + viewport: customViewport, + })).toBe(true); + expect(isPartiallyVisible({ + target: inHome4.page.withMargin, + destination: scrolled, + viewport: customViewport, + })).toBe(true); + // this one is really important - because we will be forcing it to be true + expect(isPartiallyVisible({ + target: inHome3.page.withMargin, + destination: scrolled, + viewport: customViewport, + })).toBe(false); + // this one will remain invisible + expect(isPartiallyVisible({ + target: inHome2.page.withMargin, + destination: scrolled, + viewport: customViewport, + })).toBe(false); + + const expected: DragImpact = { + movement: { + // ordered by most recently impacted + displaced: [ + // shouldAnimate has not changed to false - using previous impact + { + draggableId: inHome4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + // forced to be visible + { + draggableId: inHome3.descriptor.id, + isVisible: true, + shouldAnimate: false, + }, + // still not visible + { + draggableId: inHome2.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, + ], + amount: patch(axis.line, inHome1.page.withMargin[axis.size]), + isBeyondStartPosition: true, + }, + direction: axis.direction, + // is now in the second last position + destination: { + droppableId: home.descriptor.id, + index: 3, + }, + }; + + const result: ?Result = moveToNextIndex({ + isMovingForward: false, + draggableId: inHome1.descriptor.id, + previousImpact, + // roughly correct: + previousPageCenter: inHome1.page.withoutMargin.center, + draggables, + droppable: scrolled, + }); + + if (!result) { + throw new Error('Invalid test setup'); + } + + expect(result.impact).toEqual(expected); + }); + }); }); }); @@ -1454,20 +1694,6 @@ describe('move to next index', () => { }); }); }); - - describe('forced visibility displacement', () => { - it('should force the displacement of the closest item to be visible', () => { - - }); - - it('should use a previous displacement if it was visible', () => { - - }); - - it('should consider any change in the droppables scroll', () => { - - }); - }); }); }); }); From 0827090b03028729e62cf1aa9afe76fda6f28da8 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 19 Feb 2018 20:44:32 +1100 Subject: [PATCH 140/163] fixing keyboard displacement when moving closer to the start position --- .../get-forced-displaced.js | 109 +++++++++--------- .../move-to-next-index/in-foreign-list.js | 24 ++-- src/state/move-to-next-index/in-home-list.js | 23 ++-- 3 files changed, 82 insertions(+), 74 deletions(-) diff --git a/src/state/move-to-next-index/get-forced-displaced.js b/src/state/move-to-next-index/get-forced-displaced.js index 7c42873ac5..270cd0c6d4 100644 --- a/src/state/move-to-next-index/get-forced-displaced.js +++ b/src/state/move-to-next-index/get-forced-displaced.js @@ -59,15 +59,31 @@ export const withFirstAdded = ({ }; type WithLastRemoved = {| - distanceMoving: Position, + dragging: DraggableId, + isVisibleInNewLocation: boolean, previousImpact: DragImpact, droppable: DroppableDimension, draggables: DraggableDimensionMap, viewport: Area, |} +const forceVisibleDisplacement = (current: Displacement): Displacement => { + // if already visible - can use the existing displacement + if (current.isVisible) { + return current; + } + + // if not visible - immediately force visibility + return { + draggableId: current.draggableId, + isVisible: true, + shouldAnimate: false, + }; +}; + export const withFirstRemoved = ({ - distanceMoving, + dragging, + isVisibleInNewLocation, previousImpact, droppable, draggables, @@ -79,66 +95,55 @@ export const withFirstRemoved = ({ return []; } - const removed: Displacement[] = last.slice(1, last.length); + const withFirstRestored: Displacement[] = last.slice(1, last.length); - const axis: Axis = droppable.axis; - let buffer: number = Math.abs(distanceMoving[axis.line]); + // list is now empty + if (!withFirstRestored.length) { + return withFirstRestored; + } + + console.log('is visibile in new location?', isVisibleInNewLocation); + + // Simple case: no forced movement required + // no displacement visibility will be updated by this move + // so we can simply return the previous values + if (isVisibleInNewLocation) { + return withFirstRestored; + } console.group('forced'); - console.log('starting buffer', buffer); + const axis: Axis = droppable.axis; - const withUpdatedVisibility: Displacement[] = - removed.map((displacement: Displacement): Displacement => { - // we need to ensure that the previous items up to the size of the - // dragging item has a visible movement. This is because a movement - // can result in a combination of scrolls that have this effect. - // Technically we could just do this when the destination is not visible - but - // there is no harm doing it all the time for simplicity - - // It seems to provide a more accurate experience to force displacement when buffer >= 0 - // rather than just > 0. - console.log('buffer', buffer); - if (buffer >= 0) { - const current: DraggableDimension = draggables[displacement.draggableId]; - // Using the 'withoutMargin' size. When moving often we do not consider the margin - // as a part of the calculations for determining the new center position. As such, - // we do not reduce the buffer by the size of the margin. - const size: number = current.page.withoutMargin[axis.size]; - buffer -= size; + const toBeRestored: DraggableDimension = draggables[last[0].draggableId]; + const sizeOfRestored: number = toBeRestored.page.withMargin[axis.size]; + const sizeOfDragging: number = draggables[dragging].page.withMargin[axis.size]; + let buffer: number = sizeOfRestored + sizeOfDragging; + console.log('buffer start size', buffer); - // displacement was already visible - can leave it unmodified - if (displacement.isVisible) { - console.log('returning original for', displacement.draggableId); - return displacement; - } - - // the displacement was not visible - we need to force it to be visible and in - // place immediately. - console.log('forcing for', displacement.draggableId); - return { - draggableId: displacement.draggableId, - isVisible: true, - shouldAnimate: false, - }; + const withUpdatedVisibility: Displacement[] = + withFirstRestored.map((displacement: Displacement, index: number): Displacement => { + // we are ripping this one away and forcing it to move + if (index === 0) { + console.log('forcing displacement for first:', displacement.draggableId); + return forceVisibleDisplacement(displacement); } - console.log('recalculating for', displacement.draggableId, getDisplacement({ - draggable: draggables[displacement.draggableId], - destination: droppable, - previousImpact, - viewport, - })); + if (buffer > 0) { + const current: DraggableDimension = draggables[displacement.draggableId]; + const size: number = current.page.withMargin[axis.size]; + buffer -= size; + console.log('buffer losing', size, 'on', displacement.draggableId, 'it is now', buffer); - // We are outside of the buffer - we can now execute standard visibility checks - const updated: Displacement = getDisplacement({ - draggable: draggables[displacement.draggableId], - destination: droppable, - previousImpact, - viewport, - }); + return forceVisibleDisplacement(displacement); + } - return updated; + // We know that these items cannot be visible after the move + return { + draggableId: displacement.draggableId, + isVisible: false, + shouldAnimate: false, + }; }); console.groupEnd(); diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index 3825e7f690..6c9c4a8511 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -82,14 +82,19 @@ export default ({ destinationEdge, destinationAxis: droppable.axis, }); - // The full distance required to get from the previous page center to the new page center - const distanceMoving: Position = subtract(newPageCenter, previousPageCenter); - const distanceWithScroll: Position = withDroppableDisplacement(droppable, distanceMoving); + + const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ + draggable, + destination: droppable, + newPageCenter, + viewport, + }); const displaced: Displacement[] = (() => { if (isMovingForward) { return withFirstRemoved({ - distanceMoving: distanceWithScroll, + dragging: draggableId, + isVisibleInNewLocation, previousImpact, droppable, draggables, @@ -119,13 +124,6 @@ export default ({ direction: droppable.axis.direction, }; - const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newPageCenter, - viewport, - }); - if (isVisibleInNewLocation) { return { pageCenter: withDroppableDisplacement(droppable, newPageCenter), @@ -134,6 +132,10 @@ export default ({ }; } + // The full distance required to get from the previous page center to the new page center + const distanceMoving: Position = subtract(newPageCenter, previousPageCenter); + const distanceWithScroll: Position = withDroppableDisplacement(droppable, distanceMoving); + return { pageCenter: previousPageCenter, impact: newImpact, diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 8cb52f9058..e55f36c6d4 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -83,14 +83,18 @@ export default ({ destinationAxis: droppable.axis, }); - // The full distance required to get from the previous page center to the new page center - const distance: Position = subtract(newPageCenter, previousPageCenter); - const distanceWithScroll: Position = withDroppableDisplacement(droppable, distance); + const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ + draggable, + destination: droppable, + newPageCenter, + viewport, + }); const displaced: Displacement[] = (() => { if (isMovingTowardStart) { return withFirstRemoved({ - distanceMoving: distanceWithScroll, + dragging: draggableId, + isVisibleInNewLocation, previousImpact, droppable, draggables, @@ -119,13 +123,6 @@ export default ({ direction: droppable.axis.direction, }; - const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newPageCenter, - viewport, - }); - if (isVisibleInNewLocation) { return { pageCenter: withDroppableDisplacement(droppable, newPageCenter), @@ -134,6 +131,10 @@ export default ({ }; } + // The full distance required to get from the previous page center to the new page center + const distance: Position = subtract(newPageCenter, previousPageCenter); + const distanceWithScroll: Position = withDroppableDisplacement(droppable, distance); + return { pageCenter: previousPageCenter, impact: newImpact, From db1ddf486b957f7a833410fdacd98c5b36e1e1b1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 07:36:35 +1100 Subject: [PATCH 141/163] tests and docs --- README.md | 45 +++++---- docs/guides/screen-reader.md | 99 +++++++++++++++++++ ...isplaced.js => get-forced-displacement.js} | 17 +--- .../move-to-next-index/in-foreign-list.js | 5 +- src/state/move-to-next-index/in-home-list.js | 3 +- .../lift-action-and-dimension-marshal.spec.js | 3 +- test/unit/state/move-to-next-index.spec.js | 60 +++++++---- 7 files changed, 176 insertions(+), 56 deletions(-) create mode 100644 docs/guides/screen-reader.md rename src/state/move-to-next-index/{get-forced-displaced.js => get-forced-displacement.js} (86%) diff --git a/README.md b/README.md index c7c15e47e8..1720d0d33b 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,12 @@ See how beautiful it is for yourself! > We provide different links for touch devices as currently [storybook](https://github.com/storybooks/storybook) does not have a good mobile menu experience [more information](https://github.com/storybooks/storybook/issues/124) -## Upgrading from `3.x` to `4.x` +## Upgrading -You can find an upgrade guide in our [release notes](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v4.0.0). +We have created some upgrade instructions in our release notes to help you upgrade to the latest version! + +- [Upgrading from `4.x` to `5.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v5.0.0); +- [Upgrading from `3.x` to `4.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v4.0.0); ## Core characteristics @@ -39,12 +42,14 @@ You can find an upgrade guide in our [release notes](https://github.com/atlassia - Horizontal lists ↔ - Movement between lists (▤ ↔ ▤) - Mouse 🐭, keyboard 🎹 and touch 👉📱 (mobile, tablet and so on) support +- Auto scrolling - automatically scroll containers and the window as required during a drag (even with keyboard!!) +- Incredible screen reader support - we provide an greate experience for english screen readers out of the box 📦, while also providing complete customisation control and internationalisation support 💖 - Conditional [dragging](https://github.com/atlassian/react-beautiful-dnd#props-1) and [dropping](https://github.com/atlassian/react-beautiful-dnd#conditionally-dropping) - Multiple independent lists on the one page -- Independent nested lists - a list can be a child of another list, but you cannot drag items from the parent list into a child list -- Flexible item sizes - the draggable items can have different heights (vertical) or widths (horizontal)) -- Custom drag handle - you can drag a whole item by just a part of it +- Flexible item sizes - the draggable items can have different heights (vertical) or widths (horizontal) +- Custom drag handles - you can drag a whole item by just a part of it - A droppable list can be a scroll container (without a scrollable parent) or be the child of a scroll container (that also does not have a scrollable parent) +- Independent nested lists - a list can be a child of another list, but you cannot drag items from the parent list into a child list - Server side rendering compatible - Plays well with [nested interactive elements](https://github.com/atlassian/react-beautiful-dnd#interactive-child-elements-within-a-draggable) by default @@ -130,13 +135,27 @@ How it is composed: `react-beautiful-dnd` does not create any wrapper elements. This means that it will not impact the usual tab flow of a document. For example, if you are wrapping an *anchor* tag then the user will tab to the anchor directly and not an element surrounding the *anchor*. Whatever element you wrap will be given a `tab-index` to ensure that users can tab to the element to perform keyboard dragging. +### Auto scrolling + +When a user drags a `Draggable` near the edge of a scrollable `Droppable` or the `window` we automatically scroll the container as we are able to in order make room for the `Draggable`. + +#### For mouse and touch inputs + +When the center of a `Draggable` gets within a small distance from the edge of a container we start auto scrolling. As the user gets closer to the edge of the container we increase the speed of the auto scroll. This acceleration uses an easing function to exponentially increase the rate of acceleration the closer we move towards the edge. We reach a maximum rate of acceleration a small distance from the true edge of a container so that the user does not need to be extremely precise to obtain the maximum scroll speed. + +#### For keyboard dragging + +We also correctly update the scroll position as required when keyboard dragging. In order to move a `Draggable` into the correct position we can do a combination of a `Droppable` scroll, `window` scroll and manual movements to ensure the `Draggable` ends up in the correct position in response to user movement instructions. This is lit 🔥. + ### Accessibility Traditionally drag and drop interactions have been exclusively a mouse or touch interaction. This library ships with support for drag and drop interactions **using only a keyboard**. This enables power users to drive their experience entirely from the keyboard. As well as opening up these experiences to users who would have been excluded previously. In addition to supporting keyboard, we have also audited how the keyboard shortcuts interact with standard browser keyboard interactions. When the user is not dragging they can use their keyboard as they normally would. While dragging we override and disable certain browser shortcuts (such as `tab`) to ensure a fluid experience for the user. -We also provide **support for screen readers** through the `announce` function provided to all of the `hooks`. This means that users who have visual (or other) impairments are able to complete all drag and drop operations through keyboard and audio feedback. +We also provide **fantastic support for screen readers** to assist users with visual (or other) impairments. We ship with english messaging out of the box. However, you are welcome to override these messages by using the `announce` function that it provided to all of the `DragDropContext > hook` functions. + +See our [accessibility guide](TODO) for a guide on crafting useful screen reader messaging. ## Mouse dragging @@ -197,10 +216,6 @@ During a drag the following standard keyboard events are blocked to prevent a ba - **tab** tab ↹ - blocking tabbing - **enter** - blocking submission -### Limitations of keyboard dragging - -There is current limitation of keyboard dragging: **the drag will cancel if the user scrolls the window**. This could be worked around but for now it is the simplest initial approach. - ## Touch dragging `react-beautiful-dnd` supports dragging on touch devices such as mobiles and tablets. @@ -783,16 +798,6 @@ By using the approach you are able to make style changes to a `Droppable` when i Unfortunately we are [unable to apply this optimisation for you](https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9). It is a byproduct of using the function-as-child pattern. -### Auto scrolling is not provided (yet!) - -Currently auto scrolling of scroll containers is not part of this library. Auto scrolling is where the container automatically scrolls to make room for the dragging item as you drag near the edge of a scroll container. You are welcome to build your own auto scrolling list, or if you would you really like it as part of this library we could provide a auto scrolling `Droppable`. - -Users will be able to scroll a scroll container while dragging by using their trackpad or mouse wheel. - -### Keyboard dragging limitation - -Getting keyboard dragging to work with scroll containers is quite difficult. Currently there is a limitation: you cannot drag with a keyboard beyond the visible edge of a scroll container. This limitation could be removed if we introduced auto scrolling. Scrolling a container with a mouse during a keyboard drag will cancel the drag. - ## `Draggable` `Draggable` components can be dragged around and dropped onto `Droppable`s. A `Draggable` must always be contained within a `Droppable`. It is **possible** to reorder a `Draggable` within its home `Droppable` or move to another `Droppable`. It is **possible** because a `Droppable` is free to control what it allows to be dropped on it. diff --git a/docs/guides/screen-reader.md b/docs/guides/screen-reader.md new file mode 100644 index 0000000000..64cb7f1e97 --- /dev/null +++ b/docs/guides/screen-reader.md @@ -0,0 +1,99 @@ +# Screen reader guide + +`react-beautiful-dnd` ships with great screen reader support in english out of the box 📦! So if you are looking to just get started there is nothing you need to do. + +However, you have total control over all of the messages. This allows you to tailor the messaging for your particular usages as well as for internationalisation purposes. + +This guide has been written to assist you in creating your own messaging that is functional and delights users. It is possible for a user who is using a screen reader to use any input type. However, we have the screen reader experience to be focused on keyboard interactions. + +## Tone + +For the default messages we have gone for a friendly tone. We have also chosen to use personal language; preferring phases such as 'You have dropped the item' over 'Item dropped'. It is a little more wordy but is a friendlier experience. You are welcome to choose your own tone for your messaging. + +## Step 1: instructions + +When a user `tabs` to a `Draggable` we need to instruct them on how they can start a drag. We do this by using the `aria-roledescription` property on a `drag handle`. + +**Default message**: "Draggable item. Press space bar to lift" + +Things to note: + +- We tell the user that the item is draggable +- We tell the user how they can start a drag + +We do not give all the drag movement instructions at this point as they are not needed until a user starts a drag. + +The **default** message is fairly robust, however, you may prefer to substitute the word "item" for a noun that more closely matches your problem domain, such as "task" or "issue". You may also want to drop the word "item" altogether. + +## Step 2: drag start + +When a user lifts a `Draggable` by using the `spacebar` we want to tell them a few things: + +- they have lifted the item +- what position the item is in +- how to move the item around + +**Default message**: "You have lifted an item in position ${start.source.index + 1}. Use the arrow keys to move, space bar to drop, and escape to cancel." + +By default we do not say they are in position `1 of x`. This is because we do not have access to the size of the list in the current api. We have kept it like this for now to keep the api light and future proof as we move towards virtual lists. You are welcome to add the `1 of x` language yourself if you like! + +You may also want to say what list the item is in and potentially the index of the list. + +Here is an message that has a little more information: + +"You have lifted an item in position ${startPosition} of ${listLength} in the ${listName} list. Use the arrow keys to move, space bar to drop, and escape to cancel." + +You can control the message printed to the user by using the `DragDropContext` > `onDragStart` hook + +```js +onDragStart = (start: DragStart, provided: HookProvided) => { + provided.announce('My super cool message'); +} +``` + +## Step 3: drag movement + +When something changes in response to a user interaction we want to announce the current state of the drag to the user. There are a lot of different things that can happen so we will need a different message for these different stages. + +We can control the announcement by using the `DragDropContext` > `onDragUpdate` hook. + +```js +onDragUpdate = (update: DragUpdate, provided: HookProvided) => { + provided.announce('Update message'); +} +``` + +### Moved in the same list + +In this scenario the user has moved backwards or forwards within the same list. We want to instruct the user what position they are now in. + +**Default message**: "You have moved the item to position ${update.destination.index + 1}". + +You may also want to include `of ${listLength}` in your messaging. + +### Moved into a different list + +In this case we want to tell the user + +- they have moved to a new list +- some information about the new list +- what position they have moved from +- what position they are now in + +**Default message**: "You have moved the item from list ${update.source.droppableId} in position ${update.source.index + 1} to list ${update.destination.droppableId} in position ${update.destination.index + 1}". + +You will probably want to change this messaging to use some friendly text for the name of the droppable. It would also be good to say the size of the lists in the message + +Suggestion: + +"You have moved the item from list ${sourceName} in position ${lastIndex} of ${sourceLength} to list ${destinationName} in position ${newIndex} of ${destinationLength}". + +### Moved over no list + +While this is not possible to do with a keyboard, it is worth having a message for this in case a screen reader user is using a pointer for dragging. + +You will want to simply explain that they are not over a droppable area. + +**Default message**: "You are currently not dragging over any droppable area". + +## Step 3: drop diff --git a/src/state/move-to-next-index/get-forced-displaced.js b/src/state/move-to-next-index/get-forced-displacement.js similarity index 86% rename from src/state/move-to-next-index/get-forced-displaced.js rename to src/state/move-to-next-index/get-forced-displacement.js index 270cd0c6d4..d3a38408b4 100644 --- a/src/state/move-to-next-index/get-forced-displaced.js +++ b/src/state/move-to-next-index/get-forced-displacement.js @@ -64,7 +64,6 @@ type WithLastRemoved = {| previousImpact: DragImpact, droppable: DroppableDimension, draggables: DraggableDimensionMap, - viewport: Area, |} const forceVisibleDisplacement = (current: Displacement): Displacement => { @@ -87,7 +86,6 @@ export const withFirstRemoved = ({ previousImpact, droppable, draggables, - viewport, }: WithLastRemoved): Displacement[] => { const last: Displacement[] = previousImpact.movement.displaced; if (!last.length) { @@ -102,8 +100,6 @@ export const withFirstRemoved = ({ return withFirstRestored; } - console.log('is visibile in new location?', isVisibleInNewLocation); - // Simple case: no forced movement required // no displacement visibility will be updated by this move // so we can simply return the previous values @@ -111,21 +107,19 @@ export const withFirstRemoved = ({ return withFirstRestored; } - console.group('forced'); const axis: Axis = droppable.axis; - - const toBeRestored: DraggableDimension = draggables[last[0].draggableId]; - const sizeOfRestored: number = toBeRestored.page.withMargin[axis.size]; + // When we are forcing this displacement, we need to adjust the visibility of draggables + // within a particular range. This range is the size of the dragging item and the item + // that is being restored to its original + const sizeOfRestored: number = draggables[last[0].draggableId].page.withMargin[axis.size]; const sizeOfDragging: number = draggables[dragging].page.withMargin[axis.size]; let buffer: number = sizeOfRestored + sizeOfDragging; - console.log('buffer start size', buffer); const withUpdatedVisibility: Displacement[] = withFirstRestored.map((displacement: Displacement, index: number): Displacement => { // we are ripping this one away and forcing it to move if (index === 0) { - console.log('forcing displacement for first:', displacement.draggableId); return forceVisibleDisplacement(displacement); } @@ -133,7 +127,6 @@ export const withFirstRemoved = ({ const current: DraggableDimension = draggables[displacement.draggableId]; const size: number = current.page.withMargin[axis.size]; buffer -= size; - console.log('buffer losing', size, 'on', displacement.draggableId, 'it is now', buffer); return forceVisibleDisplacement(displacement); } @@ -146,8 +139,6 @@ export const withFirstRemoved = ({ }; }); - console.groupEnd(); - return withUpdatedVisibility; }; diff --git a/src/state/move-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js index 6c9c4a8511..69d5fd2206 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -1,11 +1,11 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch, subtract, absolute } from '../position'; +import { patch, subtract } from '../position'; import withDroppableDisplacement from '../with-droppable-displacement'; import moveToEdge from '../move-to-edge'; import getViewport from '../../window/get-viewport'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; -import { withFirstAdded, withFirstRemoved } from './get-forced-displaced'; +import { withFirstAdded, withFirstRemoved } from './get-forced-displacement'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import type { @@ -98,7 +98,6 @@ export default ({ previousImpact, droppable, draggables, - viewport, }); } return withFirstAdded({ diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index e55f36c6d4..0b881b4d30 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -6,7 +6,7 @@ import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location' import getViewport from '../../window/get-viewport'; // import getScrollJumpResult from './get-scroll-jump-result'; import moveToEdge from '../move-to-edge'; -import { withFirstAdded, withFirstRemoved } from './get-forced-displaced'; +import { withFirstAdded, withFirstRemoved } from './get-forced-displacement'; import type { Edge } from '../move-to-edge'; import type { Args, Result } from './move-to-next-index-types'; import type { @@ -98,7 +98,6 @@ export default ({ previousImpact, droppable, draggables, - viewport, }); } return withFirstAdded({ diff --git a/test/unit/integration/lift-action-and-dimension-marshal.spec.js b/test/unit/integration/lift-action-and-dimension-marshal.spec.js index 2baa2faa20..89d43aa338 100644 --- a/test/unit/integration/lift-action-and-dimension-marshal.spec.js +++ b/test/unit/integration/lift-action-and-dimension-marshal.spec.js @@ -1,9 +1,10 @@ // @flow import createStore from '../../../src/state/create-store'; import createDimensionMarshal from '../../../src/state/dimension-marshal/dimension-marshal'; +import createHookCaller from '../../../src/state/hooks/hook-caller'; +import type { HookCaller } from '../../../src/state/hooks/hooks-types'; import { getPreset } from '../../utils/dimension'; import { add } from '../../../src/state/position'; -import fireHooks from '../../../src/state/fire-hooks'; import { lift, clean, diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index efe5a7abc7..59713f67fb 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -448,8 +448,8 @@ describe('move to next index', () => { const crossAxisEnd: number = 100; const droppableScrollSize = { - scrollHeight: axis === vertical ? 350 : crossAxisEnd, - scrollWidth: axis === horizontal ? 350 : crossAxisEnd, + scrollHeight: axis === vertical ? 400 : crossAxisEnd, + scrollWidth: axis === horizontal ? 400 : crossAxisEnd, }; const home: DroppableDimension = getDroppableDimension({ @@ -462,7 +462,7 @@ describe('move to next index', () => { [axis.crossAxisStart]: crossAxisStart, [axis.crossAxisEnd]: crossAxisEnd, [axis.start]: 0, - [axis.end]: 350, + [axis.end]: 400, }), closest: { frameClient: getArea({ @@ -510,7 +510,6 @@ describe('move to next index', () => { }), }); - // half const inHome3: DraggableDimension = getDraggableDimension({ descriptor: { id: 'inHome3', @@ -525,7 +524,6 @@ describe('move to next index', () => { }), }); - // half as big as the frame (50px) const inHome4: DraggableDimension = getDraggableDimension({ descriptor: { id: 'inHome4', @@ -540,7 +538,6 @@ describe('move to next index', () => { }), }); - // half as big as the frame (50px) const inHome5: DraggableDimension = getDraggableDimension({ descriptor: { id: 'inHome5', @@ -555,15 +552,30 @@ describe('move to next index', () => { }), }); + const inHome6: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inHome5', + droppableId: home.descriptor.id, + index: 5, + }, + client: getArea({ + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 350, + [axis.end]: 400, + }), + }); + const draggables: DraggableDimensionMap = { [inHome1.descriptor.id]: inHome1, [inHome2.descriptor.id]: inHome2, [inHome3.descriptor.id]: inHome3, [inHome4.descriptor.id]: inHome4, [inHome5.descriptor.id]: inHome5, + [inHome6.descriptor.id]: inHome6, }; - it('should force the displacement of the items up to the distance of the movement', () => { + it('should force the displacement of the items up to the size of the item dragging and the item no longer being displaced', () => { // We have moved inHome1 to the end of the list const previousImpact: DragImpact = { movement: { @@ -572,15 +584,20 @@ describe('move to next index', () => { // the last impact would have been before the last addition. // At this point the last two items would have been visible { - draggableId: inHome5.descriptor.id, + draggableId: inHome6.descriptor.id, isVisible: true, shouldAnimate: true, }, { - draggableId: inHome4.descriptor.id, + draggableId: inHome5.descriptor.id, isVisible: true, shouldAnimate: true, }, + { + draggableId: inHome4.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, { draggableId: inHome3.descriptor.id, isVisible: false, @@ -606,6 +623,11 @@ describe('move to next index', () => { const scrolled: DroppableDimension = scrollDroppable(home, maxScroll); // validation of previous impact + expect(isPartiallyVisible({ + target: inHome6.page.withMargin, + destination: scrolled, + viewport: customViewport, + })).toBe(true); expect(isPartiallyVisible({ target: inHome5.page.withMargin, destination: scrolled, @@ -615,8 +637,7 @@ describe('move to next index', () => { target: inHome4.page.withMargin, destination: scrolled, viewport: customViewport, - })).toBe(true); - // this one is really important - because we will be forcing it to be true + })).toBe(false); expect(isPartiallyVisible({ target: inHome3.page.withMargin, destination: scrolled, @@ -635,17 +656,26 @@ describe('move to next index', () => { displaced: [ // shouldAnimate has not changed to false - using previous impact { - draggableId: inHome4.descriptor.id, + draggableId: inHome5.descriptor.id, isVisible: true, shouldAnimate: true, }, - // forced to be visible + // was not visibile - now forcing to be visible + // (within the size of the dragging item (50px) and the moving item (50px)) + { + draggableId: inHome4.descriptor.id, + isVisible: true, + shouldAnimate: false, + }, + // was not visibile - now forcing to be visible + // (within the size of the dragging item (50px) and the moving item (50px)) { draggableId: inHome3.descriptor.id, isVisible: true, shouldAnimate: false, }, // still not visible + // not within the 100px buffer { draggableId: inHome2.descriptor.id, isVisible: false, @@ -1270,10 +1300,6 @@ describe('move to next index', () => { expect(result.impact).toEqual(expectedImpact); expect(result.scrollJumpRequest).toEqual(expectedScrollJump); }); - - it.skip('should take into account any changes in the droppables scroll', () => { - // TODO - }); }); }); }); From b189cd53ab73635242c91d3b685b8722e57fb5bc Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 07:48:09 +1100 Subject: [PATCH 142/163] fixing more tests --- .../integration/hooks-integration.spec.js | 3 +- .../lift-action-and-dimension-marshal.spec.js | 2 +- test/unit/state/action-creators.spec.js | 22 +++--- .../state/auto-scroll/fluid-scroller.spec.js | 4 +- test/unit/state/get-droppable-over.spec.js | 67 +++++++++++++------ 5 files changed, 62 insertions(+), 36 deletions(-) diff --git a/test/unit/integration/hooks-integration.spec.js b/test/unit/integration/hooks-integration.spec.js index 9e824e57cf..6b4b253866 100644 --- a/test/unit/integration/hooks-integration.spec.js +++ b/test/unit/integration/hooks-integration.spec.js @@ -197,7 +197,8 @@ describe('hooks integration', () => { draggableId, type: 'DEFAULT', source, - destination: null, + // did not move anywhere + destination: source, reason: 'DROP', }; diff --git a/test/unit/integration/lift-action-and-dimension-marshal.spec.js b/test/unit/integration/lift-action-and-dimension-marshal.spec.js index 89d43aa338..02dee53b11 100644 --- a/test/unit/integration/lift-action-and-dimension-marshal.spec.js +++ b/test/unit/integration/lift-action-and-dimension-marshal.spec.js @@ -131,7 +131,7 @@ describe('lifting and the dimension marshal', () => { return; } - caller(hooks, previousValue, current); + caller.onStateChange(hooks, previousValue, current); dimensionMarshal.onPhaseChange(current); }); diff --git a/test/unit/state/action-creators.spec.js b/test/unit/state/action-creators.spec.js index 630b55b9b0..1aacdf68ee 100644 --- a/test/unit/state/action-creators.spec.js +++ b/test/unit/state/action-creators.spec.js @@ -7,8 +7,8 @@ import { prepare, completeLift, requestDimensions, - publishDraggableDimensions, - publishDroppableDimensions, + publishDraggableDimension, + publishDroppableDimension, } from '../../../src/state/action-creators'; import createStore from '../../../src/state/create-store'; import { getPreset } from '../../utils/dimension'; @@ -32,21 +32,21 @@ type LiftFnArgs = {| id: DraggableId, client: InitialDragPositions, windowScroll: Position, - isScrollAllowed: boolean, + autoScrollMode: 'FLUID' | 'JUMP', |} const liftDefaults: LiftFnArgs = { id: preset.inHome1.descriptor.id, windowScroll: origin, client: noWhere, - isScrollAllowed: true, + autoScrollMode: 'FLUID', }; const state = getStatePreset(); const liftWithDefaults = (args?: LiftFnArgs = liftDefaults) => { - const { id, client, windowScroll, isScrollAllowed } = args; - return lift(id, client, windowScroll, isScrollAllowed); + const { id, client, windowScroll, autoScrollMode } = args; + return lift(id, client, windowScroll, autoScrollMode); }; describe('action creators', () => { @@ -80,8 +80,8 @@ describe('action creators', () => { expect(store.dispatch).toHaveBeenCalledTimes(2); // publishing some fake dimensions - store.dispatch(publishDroppableDimensions([preset.home])); - store.dispatch(publishDraggableDimensions([preset.inHome1])); + store.dispatch(publishDroppableDimension(preset.home)); + store.dispatch(publishDraggableDimension(preset.inHome1)); // now called four times expect(store.dispatch).toHaveBeenCalledTimes(4); @@ -92,7 +92,7 @@ describe('action creators', () => { liftDefaults.id, liftDefaults.client, liftDefaults.windowScroll, - liftDefaults.isScrollAllowed + liftDefaults.autoScrollMode, )); expect(store.dispatch).toHaveBeenCalledTimes(5); }); @@ -154,7 +154,7 @@ describe('action creators', () => { liftDefaults.id, liftDefaults.client, liftDefaults.windowScroll, - liftDefaults.isScrollAllowed + liftDefaults.autoScrollMode, ) ); @@ -195,7 +195,7 @@ describe('action creators', () => { liftDefaults.id, liftDefaults.client, liftDefaults.windowScroll, - liftDefaults.isScrollAllowed + liftDefaults.autoScrollMode, )); // being super careful diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index eecae1e13b..200873bc39 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -102,7 +102,7 @@ describe('fluid auto scrolling', () => { const dragTo = ( selection: Position, // seeding that we are over the home droppable - impact?: DragImpact = getInitialImpact(axis, preset.inHome1), + impact?: DragImpact = getInitialImpact(preset.inHome1, axis), ): State => withImpact( state.dragging(preset.inHome1.descriptor.id, selection), impact, @@ -1095,7 +1095,7 @@ describe('fluid auto scrolling', () => { 2500, ); - it.only('should scroll a frame if it is being dragged over, even if not over the subject', () => { + it('should scroll a frame if it is being dragged over, even if not over the subject', () => { const scrolled: DroppableDimension = scrollDroppable( withSmallSubject, // scrolling the whole client away diff --git a/test/unit/state/get-droppable-over.spec.js b/test/unit/state/get-droppable-over.spec.js index 40d496e94a..b8748306b1 100644 --- a/test/unit/state/get-droppable-over.spec.js +++ b/test/unit/state/get-droppable-over.spec.js @@ -87,10 +87,16 @@ describe('get droppable over', () => { client: getArea({ top: 0, left: 0, right: 100, bottom: 100, }), - // will partially hide the subject - frameClient: getArea({ - top: 0, left: 0, right: 50, bottom: 100, - }), + closest: { + // will partially hide the subject + frameClient: getArea({ + top: 0, left: 0, right: 50, bottom: 100, + }), + scrollHeight: 100, + scrollWidth: 100, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const draggable: DraggableDimension = getDraggableDimension({ descriptor: { @@ -124,10 +130,17 @@ describe('get droppable over', () => { client: getArea({ top: 0, left: 0, right: 100, bottom: 100, }), - // will totally hide the subject - frameClient: getArea({ - top: 0, left: 101, right: 200, bottom: 100, - }), + closest: { + // will partially hide the subject + // will totally hide the subject + frameClient: getArea({ + top: 0, left: 101, right: 200, bottom: 100, + }), + scrollHeight: 100, + scrollWidth: 200, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const draggable: DraggableDimension = getDraggableDimension({ descriptor: { @@ -458,12 +471,18 @@ describe('get droppable over', () => { // cut off by the frame bottom: 120, }), - frameClient: getArea({ - top: 0, - left: 0, - right: 100, - bottom: 100, - }), + closest: { + frameClient: getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + scrollHeight: 120, + scrollWidth: 100, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); // scrolling custom down so that it the bottom is visible const scrolled: DroppableDimension = scrollDroppable(custom, { x: 0, y: 20 }); @@ -503,13 +522,19 @@ describe('get droppable over', () => { // this will ensure that there is required growth in the droppable bottom: inHome1.page.withMargin.height - 1, }), - frameClient: getArea({ - top: 0, - left: 0, - right: 100, - // currently much bigger than client - bottom: 500, - }), + closest: { + frameClient: getArea({ + top: 0, + left: 0, + right: 100, + // currently much bigger than client + bottom: 500, + }), + scrollWidth: 100, + scrollHeight: 500, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const scrolled: DroppableDimension = scrollDroppable(foreign, { x: 0, y: 50 }); const clipped: ?Area = scrolled.viewport.clipped; From 6dbd32f80001d8ba0450c5d7587660c6cf196562 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 07:53:02 +1100 Subject: [PATCH 143/163] fixing more tests --- test/unit/state/visibility/is-partially-visible.spec.js | 8 +++++++- test/unit/view/draggable-dimension-publisher.spec.js | 1 + test/unit/view/droppable-dimension-publisher.spec.js | 1 + test/utils/get-context-options.js | 5 +++-- test/utils/get-simple-state-preset.js | 2 ++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/test/unit/state/visibility/is-partially-visible.spec.js b/test/unit/state/visibility/is-partially-visible.spec.js index 6aa5a28e40..ef21196282 100644 --- a/test/unit/state/visibility/is-partially-visible.spec.js +++ b/test/unit/state/visibility/is-partially-visible.spec.js @@ -249,7 +249,13 @@ describe('is partially visible', () => { // stretches out past frame bottom: 600, }), - frameClient: getArea(frame), + closest: { + frameClient: getArea(frame), + scrollHeight: 600, + scrollWidth: getArea(frame).width, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); const inSubjectOutsideFrame: Spacing = { ...frame, diff --git a/test/unit/view/draggable-dimension-publisher.spec.js b/test/unit/view/draggable-dimension-publisher.spec.js index 0bde413244..b1f06985e0 100644 --- a/test/unit/view/draggable-dimension-publisher.spec.js +++ b/test/unit/view/draggable-dimension-publisher.spec.js @@ -77,6 +77,7 @@ const getMarshalStub = (): DimensionMarshal => ({ unregisterDroppable: jest.fn(), updateDroppableScroll: jest.fn(), updateDroppableIsEnabled: jest.fn(), + scrollDroppable: jest.fn(), onPhaseChange: jest.fn(), }); diff --git a/test/unit/view/droppable-dimension-publisher.spec.js b/test/unit/view/droppable-dimension-publisher.spec.js index a54ddccf83..dae53e819e 100644 --- a/test/unit/view/droppable-dimension-publisher.spec.js +++ b/test/unit/view/droppable-dimension-publisher.spec.js @@ -97,6 +97,7 @@ const getMarshalStub = (): DimensionMarshal => ({ updateDroppableScroll: jest.fn(), updateDroppableIsEnabled: jest.fn(), onPhaseChange: jest.fn(), + scrollDroppable: jest.fn(), }); describe('DraggableDimensionPublisher', () => { diff --git a/test/utils/get-context-options.js b/test/utils/get-context-options.js index c8bc59bb30..8624f2f107 100644 --- a/test/utils/get-context-options.js +++ b/test/utils/get-context-options.js @@ -54,10 +54,11 @@ export const withDimensionMarshal = (marshal?: DimensionMarshal): Object => ({ context: { [dimensionMarshalKey]: marshal || createDimensionMarshal({ cancel: () => { }, - publishDraggables: () => { }, - publishDroppables: () => { }, + publishDraggable: () => { }, + publishDroppable: () => { }, updateDroppableScroll: () => { }, updateDroppableIsEnabled: () => { }, + bulkPublish: () => { }, }), }, childContextTypes: { diff --git a/test/utils/get-simple-state-preset.js b/test/utils/get-simple-state-preset.js index 96e3bde1e8..5e5356d196 100644 --- a/test/utils/get-simple-state-preset.js +++ b/test/utils/get-simple-state-preset.js @@ -99,6 +99,7 @@ export default (axis?: Axis = vertical) => { page: clientPositions, windowScroll: origin, shouldAnimate: false, + hasCompletedFirstBulkPublish: true, }, impact: noImpact, scrollJumpRequest: null, @@ -155,6 +156,7 @@ export default (axis?: Axis = vertical) => { page: clientPositions, windowScroll: origin, shouldAnimate: true, + hasCompletedFirstBulkPublish: true, }, impact, scrollJumpRequest: request, From 436ce4f146bb53bc3c936195a909ec470398aec1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 09:51:45 +1100 Subject: [PATCH 144/163] more docs and tests --- docs/guides/screen-reader.md | 44 +- src/state/hooks/message-preset.js | 2 +- .../droppable-dimension-publisher.jsx | 10 +- .../droppable-dimension-publisher.spec.js | 517 +++++++++++------- 4 files changed, 379 insertions(+), 194 deletions(-) diff --git a/docs/guides/screen-reader.md b/docs/guides/screen-reader.md index 64cb7f1e97..feb98691e5 100644 --- a/docs/guides/screen-reader.md +++ b/docs/guides/screen-reader.md @@ -10,6 +10,10 @@ This guide has been written to assist you in creating your own messaging that is For the default messages we have gone for a friendly tone. We have also chosen to use personal language; preferring phases such as 'You have dropped the item' over 'Item dropped'. It is a little more wordy but is a friendlier experience. You are welcome to choose your own tone for your messaging. +## `HookProvided` > `Announce` + +The `announce` function that is provided to each of the `Hook` functions can be used to provide your own screen reader message. This message will be immediately read out. In order to provide a fast and responsive experience to users **you must provide this message sycnously**. If you attempt to hold onto the `announce` function and call it later it will not work and will just print a warning to the console. Additionally, if you try to call `announce` twice for the same event then only the first will be read by the screen reader with subsequent calls to `announce` being ignored and a warning printed. + ## Step 1: instructions When a user `tabs` to a `Draggable` we need to instruct them on how they can start a drag. We do this by using the `aria-roledescription` property on a `drag handle`. @@ -94,6 +98,42 @@ While this is not possible to do with a keyboard, it is worth having a message f You will want to simply explain that they are not over a droppable area. -**Default message**: "You are currently not dragging over any droppable area". +**Default message**: "You are currently not dragging over a droppable area". + +## Step 3: on drop + +In this phase we give a small summary of what the user has achieved. + +There are two ways a drop can occur. Either the drag was cancelled or the user released the dragging item. You are able to control the messaging for these events using the `DragDropContext` > `onDragEnd` hook. + +### Cancel + +A `DropResult` object has a `reason` property which can either be `DROP` or `CANCEL`. You can use this to display your cancel annoucement. + +```js +onDragEnd = (result: DropResult, provided: HookProvided) => { + if(result.reason === 'CANCEL') { + provided.announce('Your cancel message'); + return; + } +} +``` + +This announcement should: + +- Inform the user that the drag have been cancelled +- Let the user know where the item has returned to + +**Default message**: "Movement cancelled. The item has returned to its starting position of ${result.source.index + 1}" + +You are also welcome to add information about the size of the list, and the name of the list you have dropped into. + +**Suggestion** "Movement cancelled. The item has returned to list ${listName} to its starting position of ${startPosition} of ${listLength}". + + +### Drop: in the home list + +### Drop: in a foreign list + +### Drop: no destination -## Step 3: drop diff --git a/src/state/hooks/message-preset.js b/src/state/hooks/message-preset.js index 7e0d557e42..53d7093b6a 100644 --- a/src/state/hooks/message-preset.js +++ b/src/state/hooks/message-preset.js @@ -20,7 +20,7 @@ const onDragStart = (start: DragStart): string => ` const onDragUpdate = (update: DragUpdate): string => { if (!update.destination) { - return 'You are currently not dragging over any droppable area'; + return 'You are currently not dragging over a droppable area'; } // Moving in the same list diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 40c62f1dd4..365a30cc23 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -84,26 +84,24 @@ export default class DroppableDimensionPublisher extends Component { marshal.updateDroppableScroll(this.publishedDescriptor.id, newScroll); }); + // TODO: when keyboard dragging we probably want this to be instant! scheduleScrollUpdate = rafSchedule(() => { // Capturing the scroll now so that it is the latest value const offset: Position = this.getClosestScroll(); this.memoizedUpdateScroll(offset.x, offset.y); }); - // scheduleScrollUpdate = () => { - // // Capturing the scroll now so that it is the latest value - // const offset: Position = this.getClosestScroll(); - // this.memoizedUpdateScroll(offset.x, offset.y); - // }; onClosestScroll = () => this.scheduleScrollUpdate(); scroll = (change: Position) => { if (this.closestScrollable == null) { + console.error('Cannot scroll a droppable with no closest scrollable'); return; } if (!this.isWatchingScroll) { - console.warn('Updating Droppable scroll while not watching for updates'); + console.error('Updating Droppable scroll while not watching for updates'); + return; } this.closestScrollable.scrollTop += change.y; diff --git a/test/unit/view/droppable-dimension-publisher.spec.js b/test/unit/view/droppable-dimension-publisher.spec.js index dae53e819e..9f883beafc 100644 --- a/test/unit/view/droppable-dimension-publisher.spec.js +++ b/test/unit/view/droppable-dimension-publisher.spec.js @@ -89,6 +89,86 @@ class ScrollableItem extends Component } } +type AppProps = { + droppableIsScrollable?: boolean, + parentIsScrollable?: boolean, + ignoreContainerClipping: boolean, +}; +type AppState = { + ref: ?HTMLElement, +} + +const frame: Area = getArea({ + top: 0, + left: 0, + right: 150, + bottom: 150, +}); +const client: Area = getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, +}); +const descriptor: DroppableDescriptor = { + id: 'a cool droppable', + type: 'cool', +}; + +class App extends Component { + static defaultProps = { + onPublish: () => {}, + ignoreContainerClipping: false, + } + + state = { ref: null } + setRef = ref => this.setState({ ref }) + render() { + const { + droppableIsScrollable, + parentIsScrollable, + ignoreContainerClipping, + } = this.props; + return ( +
+
+
+ +
hello world
+
+
+
+
+ ); + } +} + const getMarshalStub = (): DimensionMarshal => ({ registerDraggable: jest.fn(), unregisterDraggable: jest.fn(), @@ -364,234 +444,206 @@ describe('DraggableDimensionPublisher', () => { expect(result).toEqual(expected); }); - it('should capture the initial scroll containers current scroll', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const frameScroll: Position = { - x: 500, - y: 1000, - }; - const expected: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'my-fake-id', - type: 'fake', - }, - client: getArea({ - top: 0, - right: 100, - bottom: 200, - left: 0, - }), - frameScroll, - }); - jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ - top: expected.page.withoutMargin.top, - bottom: expected.page.withoutMargin.bottom, - left: expected.page.withoutMargin.left, - right: expected.page.withoutMargin.right, - height: expected.page.withoutMargin.height, - width: expected.page.withoutMargin.width, - })); - jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({ - overflow: 'auto', - ...noSpacing, - })); - const wrapper = mount( - , - withDimensionMarshal(marshal) - ); - // setting initial scroll - const container: HTMLElement = wrapper.getDOMNode(); - container.scrollLeft = frameScroll.x; - container.scrollTop = frameScroll.y; - - // pull the get dimension function out - const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimension(); + describe('closest scrollable', () => { + describe('no closest scrollable', () => { + it('should return null for the closest scrollable if there is no scroll container', () => { + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + client, + }); + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const droppableNode = wrapper.state().ref; + jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client); - expect(result).toEqual(expected); - }); + // pull the get dimension function out + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimension(); - describe('calculating the frame', () => { - const frame: Area = getArea({ - top: 0, - left: 0, - right: 150, - bottom: 150, + expect(result).toEqual(expected); + }); }); - const client: Area = getArea({ - top: 0, - left: 0, - right: 100, - bottom: 100, - }); - const descriptor: DroppableDescriptor = { - id: 'a cool droppable', - type: 'cool', - }; - - const dimensionWithoutScrollParent: DroppableDimension = getDroppableDimension({ - descriptor, - client, - }); - const dimensionWithScrollParent: DroppableDimension = getDroppableDimension({ - descriptor, - client, - frameClient: frame, - }); - - type AppProps = { - droppableIsScrollable?: boolean, - parentIsScrollable?: boolean, - ignoreContainerClipping: boolean, - }; - type AppState = { - ref: ?HTMLElement, - } - - class App extends Component { - static defaultProps = { - onPublish: () => {}, - ignoreContainerClipping: false, - } - state = { ref: null } - setRef = ref => this.setState({ ref }) - render() { - const { - droppableIsScrollable, - parentIsScrollable, - ignoreContainerClipping, - } = this.props; - return ( -
-
-
- -
hello world
-
-
-
-
+ describe('droppable is scrollable', () => { + it('should capture the frame', () => { + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + client: frame, + closest: { + frameClient: frame, + scrollWidth: frame.width, + scrollHeight: frame.height, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const marshal: DimensionMarshal = getMarshalStub(); + // both the droppable and the parent are scrollable + const wrapper = mount( + , + withDimensionMarshal(marshal), ); - } - } - - it('should detect a scrollable parent', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const parentNode = wrapper.getDOMNode(); - const droppableNode = wrapper.state().ref; - jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame); - jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client); + const droppableNode = wrapper.state().ref; + jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => frame); - // pull the get dimension function out - const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimension(); + // pull the get dimension function out + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimension(); - expect(result).toEqual(dimensionWithScrollParent); + expect(result).toEqual(expected); + }); }); - it('should ignore any parents if they are not scroll containers', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const parentNode = wrapper.getDOMNode(); - const droppableNode = wrapper.state().ref; - jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame); - jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client); - - // pull the get dimension function out - const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimension(); + describe('parent of droppable is scrollable', () => { + it('should capture the frame', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const parentNode = wrapper.getDOMNode(); + const droppableNode = wrapper.state().ref; + jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame); + jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client); + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + client, + closest: { + frameClient: frame, + scrollWidth: frame.width, + scrollHeight: frame.height, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimension(); + + expect(result).toEqual(expected); + }); + }); - expect(result).toEqual(dimensionWithoutScrollParent); + describe('both droppable and parent is scrollable', () => { + it('should only consider the closest scrollable - which is the droppable', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const parentNode = wrapper.getDOMNode(); + const droppableNode = wrapper.state().ref; + jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame); + jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client); + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + client, + closest: { + frameClient: client, + scrollWidth: client.width, + scrollHeight: client.height, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimension(); + + expect(result).toEqual(expected); + }); }); - it('should use itself as the frame if the droppable is scrollable', () => { + it('should capture the initial scroll of the scrollest scrollable', () => { + // in this case the parent of the droppable is the closest scrollable + const frameScroll: Position = { x: 10, y: 20 }; const marshal: DimensionMarshal = getMarshalStub(); - // both the droppable and the parent are scrollable const wrapper = mount( , withDimensionMarshal(marshal), ); - const parentNode = wrapper.getDOMNode(); const droppableNode = wrapper.state().ref; + const parentNode = wrapper.getDOMNode(); + // manually setting the scroll of the parent node + parentNode.scrollTop = frameScroll.y; + parentNode.scrollLeft = frameScroll.x; jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame); jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client); - - // pull the get dimension function out + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + client, + closest: { + frameClient: frame, + scrollWidth: frame.width, + scrollHeight: frame.height, + scroll: frameScroll, + shouldClipSubject: true, + }, + }); + + // pull the get dimension function out const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; // execute it to get the dimension const result: DroppableDimension = callbacks.getDimension(); - expect(result).toEqual(dimensionWithoutScrollParent); + expect(result).toEqual(expected); }); - it('should return ignore the parent frame when ignoreContainerClipping is set', () => { + it('should indicate if subject clipping is permitted based on the ignoreContainerClipping prop', () => { + // in this case the parent of the droppable is the closest scrollable const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( , withDimensionMarshal(marshal), ); - const parentNode = wrapper.getDOMNode(); const droppableNode = wrapper.state().ref; + const parentNode = wrapper.getDOMNode(); jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame); jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client); - - // pull the get dimension function out + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + client, + closest: { + frameClient: frame, + scrollWidth: frame.width, + scrollHeight: frame.height, + scroll: { x: 0, y: 0 }, + shouldClipSubject: false, + }, + }); + + // pull the get dimension function out const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; // execute it to get the dimension const result: DroppableDimension = callbacks.getDimension(); - expect(result).toEqual(dimensionWithoutScrollParent); + expect(result).toEqual(expected); }); }); }); @@ -790,6 +842,101 @@ describe('DraggableDimensionPublisher', () => { }); }); + describe('forced scroll', () => { + it('should not do anything if the droppable has no closest scrollable', () => { + const marshal: DimensionMarshal = getMarshalStub(); + // no scroll parent + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const parentNode = wrapper.getDOMNode(); + const droppableNode = wrapper.state().ref; + jest.spyOn(parentNode, 'getBoundingClientRect').mockImplementation(() => frame); + jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementation(() => client); + + // validating no initial scroll + expect(parentNode.scrollTop).toBe(0); + expect(parentNode.scrollLeft).toBe(0); + expect(droppableNode.scrollTop).toBe(0); + expect(droppableNode.scrollLeft).toBe(0); + + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + // request the droppable start listening for scrolling + callbacks.getDimension(); + callbacks.watchScroll(); + expect(console.error).not.toHaveBeenCalled(); + + // ask it to scroll + callbacks.scroll({ x: 100, y: 100 }); + + expect(parentNode.scrollTop).toBe(0); + expect(parentNode.scrollLeft).toBe(0); + expect(droppableNode.scrollTop).toBe(0); + expect(droppableNode.scrollLeft).toBe(0); + expect(console.error).toHaveBeenCalled(); + }); + + describe('there is a closest scrollable', () => { + it('should update the scroll of the closest scrollable', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const container: HTMLElement = wrapper.getDOMNode(); + + if (!container.classList.contains('scroll-container')) { + throw new Error('incorrect dom node collected'); + } + + expect(container.scrollTop).toBe(0); + expect(container.scrollLeft).toBe(0); + + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + // watch scroll will only be called after the dimension is requested + callbacks.getDimension(); + callbacks.watchScroll(); + + callbacks.scroll({ x: 500, y: 1000 }); + + expect(container.scrollLeft).toBe(500); + expect(container.scrollTop).toBe(1000); + }); + + it('should not scroll if scroll is not currently being watched', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const container: HTMLElement = wrapper.getDOMNode(); + + if (!container.classList.contains('scroll-container')) { + throw new Error('incorrect dom node collected'); + } + + expect(container.scrollTop).toBe(0); + expect(container.scrollLeft).toBe(0); + + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + callbacks.getDimension(); + // not watching scroll yet + + callbacks.scroll({ x: 500, y: 1000 }); + + expect(container.scrollLeft).toBe(0); + expect(container.scrollTop).toBe(0); + expect(console.error).toHaveBeenCalled(); + }); + }); + }); + describe('is enabled changes', () => { it('should publish updates to the enabled state', () => { const marshal: DimensionMarshal = getMarshalStub(); From dad905bebd432484eab1c712e7997466629e2a68 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 11:01:48 +1100 Subject: [PATCH 145/163] immediate droppable scrolling for keyboard dragging --- src/state/action-creators.js | 17 +- .../dimension-marshal-types.js | 3 +- .../dimension-marshal/dimension-marshal.js | 37 ++- src/state/reducer.js | 5 +- src/types.js | 11 +- .../droppable-dimension-publisher.jsx | 27 +- .../droppable-dimension-publisher.spec.js | 306 +++++++++++------- 7 files changed, 261 insertions(+), 145 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index f66eda6b59..e7893c6717 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -14,7 +14,9 @@ import type { CurrentDrag, InitialDrag, DraggableDescriptor, + InitialLiftRequest, AutoScrollMode, + ScrollOptions, } from '../types'; import noImpact from './no-impact'; import withDroppableDisplacement from './with-droppable-displacement'; @@ -48,12 +50,12 @@ const getScrollDiff = ({ export type RequestDimensionsAction = {| type: 'REQUEST_DIMENSIONS', - payload: DraggableId, + payload: InitialLiftRequest, |} -export const requestDimensions = (id: DraggableId): RequestDimensionsAction => ({ +export const requestDimensions = (request: InitialLiftRequest): RequestDimensionsAction => ({ type: 'REQUEST_DIMENSIONS', - payload: id, + payload: request, }); export type CompleteLiftAction = {| @@ -490,7 +492,14 @@ export const lift = (id: DraggableId, } // will communicate with the marshal to start requesting dimensions - dispatch(requestDimensions(id)); + const scrollOptions: ScrollOptions = { + shouldPublishImmediately: autoScrollMode === 'JUMP', + }; + const request: InitialLiftRequest = { + draggableId: id, + scrollOptions, + }; + dispatch(requestDimensions(request)); // Need to allow an opportunity for the dimensions to be requested. setTimeout(() => { diff --git a/src/state/dimension-marshal/dimension-marshal-types.js b/src/state/dimension-marshal/dimension-marshal-types.js index aaab09a4e9..c144bed584 100644 --- a/src/state/dimension-marshal/dimension-marshal-types.js +++ b/src/state/dimension-marshal/dimension-marshal-types.js @@ -8,6 +8,7 @@ import type { DroppableId, State, Position, + ScrollOptions, } from '../../types'; export type GetDraggableDimensionFn = () => DraggableDimension; @@ -20,7 +21,7 @@ export type DroppableCallbacks = {| // Droppable must listen to scroll events and publish them using the // onChange callback. If the Droppable is not in a scroll container then // it does not need to do anything - watchScroll: () => void, + watchScroll: (options: ScrollOptions) => void, // If the Droppable is listening for scrol events - it needs to stop! // This may be called even if watchScroll was not previously called unwatchScroll: () => void, diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index 6268d9626f..01f031d541 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -9,6 +9,8 @@ import type { State as AppState, Phase, Position, + InitialLiftRequest, + ScrollOptions, } from '../../types'; import type { DimensionMarshal, @@ -27,8 +29,10 @@ type State = {| // long lived droppables: DroppableEntryMap, draggables: DraggableEntryMap, + // short lived isCollecting: boolean, - request: ?DraggableId, + scrollOptions: ?ScrollOptions, + request: ?InitialLiftRequest, frameId: ?number, |} @@ -42,6 +46,7 @@ export default (callbacks: Callbacks) => { droppables: {}, draggables: {}, isCollecting: false, + scrollOptions: null, request: null, frameId: null, }; @@ -237,14 +242,14 @@ export default (callbacks: Callbacks) => { const getToBeCollected = (): UnknownDescriptorType[] => { const draggables: DraggableEntryMap = state.draggables; const droppables: DroppableEntryMap = state.droppables; - const request: ?DraggableId = state.request; + const request: ?InitialLiftRequest = state.request; if (!request) { console.error('cannot find request in state'); return []; } - - const descriptor: DraggableDescriptor = draggables[request].descriptor; + const draggableId: DraggableId = request.draggableId; + const descriptor: DraggableDescriptor = draggables[draggableId].descriptor; const home: DroppableDescriptor = droppables[descriptor.droppableId].descriptor; const draggablesToBeCollected: DraggableDescriptor[] = @@ -285,7 +290,7 @@ export default (callbacks: Callbacks) => { return toBeCollected; }; - const processPrimaryDimensions = (request: ?DraggableId) => { + const processPrimaryDimensions = (request: ?InitialLiftRequest) => { if (state.isCollecting) { cancel('Cannot start capturing dimensions for a drag it is already dragging'); return; @@ -296,6 +301,8 @@ export default (callbacks: Callbacks) => { return; } + const draggableId: DraggableId = request.draggableId; + setState({ isCollecting: true, request, @@ -303,17 +310,20 @@ export default (callbacks: Callbacks) => { const draggables: DraggableEntryMap = state.draggables; const droppables: DroppableEntryMap = state.droppables; - const draggableEntry: ?DraggableEntry = draggables[request]; + const draggableEntry: ?DraggableEntry = draggables[draggableId]; if (!draggableEntry) { - cancel(`Cannot find Draggable with id ${request} to start collecting dimensions`); + cancel(`Cannot find Draggable with id ${draggableId} to start collecting dimensions`); return; } const homeEntry: ?DroppableEntry = droppables[draggableEntry.descriptor.droppableId]; if (!homeEntry) { - cancel(`Cannot find home Droppable [id:${draggableEntry.descriptor.droppableId}] for Draggable [id:${request}]`); + cancel(` + Cannot find home Droppable [id:${draggableEntry.descriptor.droppableId}] + for Draggable [id:${request.draggableId}] + `); return; } @@ -324,7 +334,7 @@ export default (callbacks: Callbacks) => { callbacks.publishDroppable(home); callbacks.publishDraggable(draggable); // Watching the scroll of the home droppable - homeEntry.callbacks.watchScroll(); + homeEntry.callbacks.watchScroll(request.scrollOptions); }; const setFrameId = (frameId: ?number) => { @@ -339,6 +349,13 @@ export default (callbacks: Callbacks) => { return; } + const request: ?InitialLiftRequest = state.request; + + if (!request) { + console.error('Cannot process secondary dimensions without a request'); + return; + } + const toBeCollected: UnknownDescriptorType[] = getToBeCollected(); // Phase 1: collect dimensions in a single frame @@ -376,7 +393,7 @@ export default (callbacks: Callbacks) => { // need to watch the scroll on each droppable toBePublished.droppables.forEach((dimension: DroppableDimension) => { const entry: DroppableEntry = state.droppables[dimension.descriptor.id]; - entry.callbacks.watchScroll(); + entry.callbacks.watchScroll(request.scrollOptions); }); setFrameId(null); diff --git a/src/state/reducer.js b/src/state/reducer.js index 56e0f4ad65..9e7e74fd10 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -22,6 +22,7 @@ import type { CurrentDragPositions, Position, InitialDragPositions, + InitialLiftRequest, } from '../types'; import { add, subtract, isEqual } from './position'; import { noMovement } from './no-impact'; @@ -173,14 +174,14 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } - const id: DraggableId = action.payload; + const request: InitialLiftRequest = action.payload; return { phase: 'COLLECTING_INITIAL_DIMENSIONS', drag: null, drop: null, dimension: { - request: id, + request, draggable: {}, droppable: {}, }, diff --git a/src/types.js b/src/types.js index e805987943..af087f3392 100644 --- a/src/types.js +++ b/src/types.js @@ -289,13 +289,22 @@ export type Phase = // This will result in the onDragEnd hook being fired 'DROP_COMPLETE'; +export type ScrollOptions = {| + shouldPublishImmediately: boolean, +|} + +export type InitialLiftRequest = {| + draggableId: DraggableId, + scrollOptions: ScrollOptions, +|} + export type DimensionState = {| // using the draggable id rather than the descriptor as the descriptor // may change as a result of the initial flush. This means that the lift // descriptor may not be the same as the actual descriptor. To avoid // confusion the request is just an id which is looked up // in the dimension-marshal post-flush - request: ?DraggableId, + request: ?InitialLiftRequest, draggable: DraggableDimensionMap, droppable: DroppableDimensionMap, |}; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 365a30cc23..64705b45d8 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -9,7 +9,6 @@ import getArea from '../../state/get-area'; import { getDroppableDimension } from '../../state/dimension'; import getClosestScrollable from '../get-closest-scrollable'; import { dimensionMarshalKey } from '../context-keys'; -import { apply } from '../../state/position'; import type { DimensionMarshal, DroppableCallbacks, @@ -23,6 +22,7 @@ import type { Area, Spacing, Direction, + ScrollOptions, } from '../../types'; type Props = {| @@ -42,6 +42,7 @@ export default class DroppableDimensionPublisher extends Component { /* eslint-disable react/sort-comp */ closestScrollable: ?Element = null; isWatchingScroll: boolean = false; + scrollOptions: ?ScrollOptions = null; callbacks: DroppableCallbacks; publishedDescriptor: ?DroppableDescriptor = null; @@ -84,14 +85,24 @@ export default class DroppableDimensionPublisher extends Component { marshal.updateDroppableScroll(this.publishedDescriptor.id, newScroll); }); - // TODO: when keyboard dragging we probably want this to be instant! - scheduleScrollUpdate = rafSchedule(() => { - // Capturing the scroll now so that it is the latest value + updateScroll = () => { const offset: Position = this.getClosestScroll(); this.memoizedUpdateScroll(offset.x, offset.y); - }); + } - onClosestScroll = () => this.scheduleScrollUpdate(); + scheduleScrollUpdate = rafSchedule(this.updateScroll); + + onClosestScroll = () => { + if (!this.scrollOptions) { + console.error('Cannot find scroll options while scrolling'); + return; + } + if (this.scrollOptions.shouldPublishImmediately) { + this.updateScroll(); + return; + } + this.scheduleScrollUpdate(); + } scroll = (change: Position) => { if (this.closestScrollable == null) { @@ -108,7 +119,7 @@ export default class DroppableDimensionPublisher extends Component { this.closestScrollable.scrollLeft += change.x; } - watchScroll = () => { + watchScroll = (options: ScrollOptions) => { if (!this.props.targetRef) { console.error('cannot watch droppable scroll if not in the dom'); return; @@ -124,6 +135,7 @@ export default class DroppableDimensionPublisher extends Component { } this.isWatchingScroll = true; + this.scrollOptions = options; this.closestScrollable.addEventListener('scroll', this.onClosestScroll, { passive: true }); }; @@ -135,6 +147,7 @@ export default class DroppableDimensionPublisher extends Component { } this.isWatchingScroll = false; + this.scrollOptions = null; this.scheduleScrollUpdate.cancel(); if (!this.closestScrollable) { diff --git a/test/unit/view/droppable-dimension-publisher.spec.js b/test/unit/view/droppable-dimension-publisher.spec.js index 9f883beafc..f55134c8ba 100644 --- a/test/unit/view/droppable-dimension-publisher.spec.js +++ b/test/unit/view/droppable-dimension-publisher.spec.js @@ -15,6 +15,7 @@ import type { } from '../../../src/state/dimension-marshal/dimension-marshal-types'; import type { Area, + ScrollOptions, Spacing, DroppableId, DroppableDimension, @@ -180,6 +181,13 @@ const getMarshalStub = (): DimensionMarshal => ({ scrollDroppable: jest.fn(), }); +const scheduled: ScrollOptions = { + shouldPublishImmediately: false, +}; +const immediate: ScrollOptions = { + shouldPublishImmediately: true, +}; + describe('DraggableDimensionPublisher', () => { const originalWindowScroll: Position = { x: window.pageXOffset, @@ -411,7 +419,7 @@ describe('DraggableDimensionPublisher', () => { y: 1000, }; setWindowScroll(windowScroll, { shouldPublish: false }); - const client: Area = getArea({ + const ourClient: Area = getArea({ top: 0, right: 100, bottom: 100, @@ -423,9 +431,9 @@ describe('DraggableDimensionPublisher', () => { type: 'fake', }, windowScroll, - client, + client: ourClient, }); - jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => client); + jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ourClient); jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing); mount( @@ -655,133 +663,198 @@ describe('DraggableDimensionPublisher', () => { el.dispatchEvent(new Event('scroll')); }; - it('should publish the scroll offset of the closest scrollable', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); + describe('should immediately publish updates', () => { + it('should immediately publish the scroll offset of the closest scrollable', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const container: HTMLElement = wrapper.getDOMNode(); - if (!container.classList.contains('scroll-container')) { - throw new Error('incorrect dom node collected'); - } + if (!container.classList.contains('scroll-container')) { + throw new Error('incorrect dom node collected'); + } - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; - // watch scroll will only be called after the dimension is requested - callbacks.getDimension(); - callbacks.watchScroll(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + // watch scroll will only be called after the dimension is requested + callbacks.getDimension(); + callbacks.watchScroll(immediate); - scroll(container, { x: 500, y: 1000 }); - // release the update animation frame - requestAnimationFrame.step(); + scroll(container, { x: 500, y: 1000 }); - expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( - preset.home.descriptor.id, { x: 500, y: 1000 }, - ); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, { x: 500, y: 1000 }, + ); + }); + + it('should not fire a scroll if the value has not changed since the previous call', () => { + // this can happen if you scroll backward and forward super quick + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + callbacks.getDimension(); + callbacks.watchScroll(immediate); + + // first event + scroll(container, { x: 500, y: 1000 }); + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, { x: 500, y: 1000 } + ); + marshal.updateDroppableScroll.mockReset(); + + // second event - scroll to same spot + scroll(container, { x: 500, y: 1000 }); + expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); + + // third event - new value + scroll(container, { x: 500, y: 1001 }); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, { x: 500, y: 1001 } + ); + }); }); - it('should throttle multiple scrolls into a animation frame', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + describe('should schedule publish updates', () => { + it('should publish the scroll offset of the closest scrollable', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const container: HTMLElement = wrapper.getDOMNode(); - // watch scroll will only be called after the dimension is requested - callbacks.getDimension(); - callbacks.watchScroll(); + if (!container.classList.contains('scroll-container')) { + throw new Error('incorrect dom node collected'); + } - // first event - scroll(container, { x: 500, y: 1000 }); - // second event in same frame - scroll(container, { x: 200, y: 800 }); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + // watch scroll will only be called after the dimension is requested + callbacks.getDimension(); + callbacks.watchScroll(scheduled); - // release the update animation frame - requestAnimationFrame.step(); + scroll(container, { x: 500, y: 1000 }); + // release the update animation frame + requestAnimationFrame.step(); - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( - preset.home.descriptor.id, { x: 200, y: 800 }, - ); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, { x: 500, y: 1000 }, + ); + }); - // also checking that no loose frames are stored up - requestAnimationFrame.flush(); - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - }); + it('should throttle multiple scrolls into a animation frame', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; - it('should not fire a scroll if the value has not changed since the previous frame', () => { - // this can happen if you scroll backward and forward super quick - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + // watch scroll will only be called after the dimension is requested + callbacks.getDimension(); + callbacks.watchScroll(scheduled); - // watch scroll will only be called after the dimension is requested - callbacks.getDimension(); - callbacks.watchScroll(); + // first event + scroll(container, { x: 500, y: 1000 }); + // second event in same frame + scroll(container, { x: 200, y: 800 }); - // first event - scroll(container, { x: 500, y: 1000 }); - // release the frame - requestAnimationFrame.step(); - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( - preset.home.descriptor.id, { x: 500, y: 1000 } - ); - marshal.updateDroppableScroll.mockReset(); + // release the update animation frame + requestAnimationFrame.step(); - // second event - scroll(container, { x: 501, y: 1001 }); - // no frame to release change yet + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, { x: 200, y: 800 }, + ); - // third event - back to original value - scroll(container, { x: 500, y: 1000 }); - // release the frame - requestAnimationFrame.step(); - expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); - }); + // also checking that no loose frames are stored up + requestAnimationFrame.flush(); + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + }); - it('should stop watching scroll when no longer required to publish', () => { - // this can happen if you scroll backward and forward super quick - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + it('should not fire a scroll if the value has not changed since the previous frame', () => { + // this can happen if you scroll backward and forward super quick + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; - // watch scroll will only be called after the dimension is requested - callbacks.getDimension(); - callbacks.watchScroll(); + // watch scroll will only be called after the dimension is requested + callbacks.getDimension(); + callbacks.watchScroll(scheduled); + + // first event + scroll(container, { x: 500, y: 1000 }); + // release the frame + requestAnimationFrame.step(); + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, { x: 500, y: 1000 } + ); + marshal.updateDroppableScroll.mockReset(); - // first event - scroll(container, { x: 500, y: 1000 }); - // release the frame - requestAnimationFrame.step(); - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - marshal.updateDroppableScroll.mockReset(); + // second event + scroll(container, { x: 501, y: 1001 }); + // no frame to release change yet - callbacks.unwatchScroll(); + // third event - back to original value + scroll(container, { x: 500, y: 1000 }); + // release the frame + requestAnimationFrame.step(); + expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); + }); - // scroll event after no longer watching - scroll(container, { x: 190, y: 400 }); - // let any frames go that want to - requestAnimationFrame.flush(); - expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); + it('should not publish a scroll update after requested not to update while an animation frame is occurring', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + callbacks.getDimension(); + callbacks.watchScroll(scheduled); + + // first event + scroll(container, { x: 500, y: 1000 }); + requestAnimationFrame.step(); + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + marshal.updateDroppableScroll.mockReset(); + + // second event + scroll(container, { x: 400, y: 100 }); + // no animation frame to release event fired yet + + // unwatching before frame fired + callbacks.unwatchScroll(); + + // flushing any frames + requestAnimationFrame.flush(); + expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); + }); }); - it('should not publish a scroll update after requested not to update while an animation frame is occurring', () => { + it('should stop watching scroll when no longer required to publish', () => { + // this can happen if you scroll backward and forward super quick const marshal: DimensionMarshal = getMarshalStub(); const wrapper = mount( , @@ -793,23 +866,17 @@ describe('DraggableDimensionPublisher', () => { // watch scroll will only be called after the dimension is requested callbacks.getDimension(); - callbacks.watchScroll(); + callbacks.watchScroll(immediate); // first event scroll(container, { x: 500, y: 1000 }); - requestAnimationFrame.step(); expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); marshal.updateDroppableScroll.mockReset(); - // second event - scroll(container, { x: 400, y: 100 }); - // no animation frame to release event fired yet - - // unwatching before frame fired callbacks.unwatchScroll(); - // flushing any frames - requestAnimationFrame.flush(); + // scroll event after no longer watching + scroll(container, { x: 190, y: 400 }); expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); }); @@ -826,13 +893,12 @@ describe('DraggableDimensionPublisher', () => { // watch scroll will only be called after the dimension is requested callbacks.getDimension(); - callbacks.watchScroll(); + callbacks.watchScroll(immediate); wrapper.unmount(); // second event - will not fire any updates scroll(container, { x: 100, y: 300 }); - requestAnimationFrame.step(); expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); // also logs a warning expect(console.warn).toHaveBeenCalled(); @@ -867,7 +933,7 @@ describe('DraggableDimensionPublisher', () => { const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; // request the droppable start listening for scrolling callbacks.getDimension(); - callbacks.watchScroll(); + callbacks.watchScroll(scheduled); expect(console.error).not.toHaveBeenCalled(); // ask it to scroll @@ -900,7 +966,7 @@ describe('DraggableDimensionPublisher', () => { const callbacks: DroppableCallbacks = marshal.registerDroppable.mock.calls[0][1]; // watch scroll will only be called after the dimension is requested callbacks.getDimension(); - callbacks.watchScroll(); + callbacks.watchScroll(scheduled); callbacks.scroll({ x: 500, y: 1000 }); From 8708b7ed35e3bccc4bf483d65aecefe6e3abda7a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 13:08:49 +1100 Subject: [PATCH 146/163] tests for scroll options --- .../dimension-marshal/dimension-marshal.js | 2 +- .../server-side-rendering.spec.js.snap | 4 +- test/unit/state/action-creators.spec.js | 16 +- test/unit/view/dimension-marshal.spec.js | 401 ++++++++++++------ test/utils/get-simple-state-preset.js | 33 +- 5 files changed, 327 insertions(+), 129 deletions(-) diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index 01f031d541..1d87a1f015 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -432,7 +432,7 @@ export default (callbacks: Callbacks) => { } if (phase === 'DRAGGING') { - if (current.dimension.request !== state.request) { + if (current.dimension.request.draggableId !== state.request.draggableId) { cancel('Request in local state does not match that of the store'); return; } diff --git a/test/unit/integration/__snapshots__/server-side-rendering.spec.js.snap b/test/unit/integration/__snapshots__/server-side-rendering.spec.js.snap index bbe2573268..c34fa743f0 100644 --- a/test/unit/integration/__snapshots__/server-side-rendering.spec.js.snap +++ b/test/unit/integration/__snapshots__/server-side-rendering.spec.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`server side rendering should support rendering to a string 1`] = `"
"`; +exports[`server side rendering should support rendering to a string 1`] = `"
"`; -exports[`server side rendering should support rendering to static markup 1`] = `"
"`; +exports[`server side rendering should support rendering to static markup 1`] = `"
"`; diff --git a/test/unit/state/action-creators.spec.js b/test/unit/state/action-creators.spec.js index 1aacdf68ee..4167d0c08c 100644 --- a/test/unit/state/action-creators.spec.js +++ b/test/unit/state/action-creators.spec.js @@ -19,6 +19,7 @@ import type { DraggableId, Store, InitialDragPositions, + InitialLiftRequest, } from '../../../src/types'; const preset = getPreset(); @@ -76,7 +77,13 @@ describe('action creators', () => { // Phase 2: request dimensions after flushing animations jest.runOnlyPendingTimers(); - expect(store.dispatch).toHaveBeenCalledWith(requestDimensions(preset.inHome1.descriptor.id)); + const request: InitialLiftRequest = { + draggableId: preset.inHome1.descriptor.id, + scrollOptions: { + shouldPublishImmediately: false, + }, + }; + expect(store.dispatch).toHaveBeenCalledWith(requestDimensions(request)); expect(store.dispatch).toHaveBeenCalledTimes(2); // publishing some fake dimensions @@ -178,7 +185,12 @@ describe('action creators', () => { jest.runOnlyPendingTimers(); expect(store.dispatch).toHaveBeenCalledWith( - requestDimensions(preset.inHome1.descriptor.id) + requestDimensions({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: { + shouldPublishImmediately: false, + }, + }) ); expect(store.dispatch).toHaveBeenCalledTimes(2); diff --git a/test/unit/view/dimension-marshal.spec.js b/test/unit/view/dimension-marshal.spec.js index 866a27d222..ce4dba1d60 100644 --- a/test/unit/view/dimension-marshal.spec.js +++ b/test/unit/view/dimension-marshal.spec.js @@ -20,6 +20,8 @@ import type { DroppableId, DraggableDescriptor, DroppableDescriptor, + ScrollOptions, + InitialLiftRequest, Area, } from '../../../src/types'; @@ -82,8 +84,8 @@ const populateMarshal = ( watches.droppable.getDimension(id); return droppable; }, - watchScroll: () => { - watches.droppable.watchScroll(id); + watchScroll: (options: ScrollOptions) => { + watches.droppable.watchScroll(id, options); }, unwatchScroll: () => { watches.droppable.unwatchScroll(id); @@ -126,6 +128,30 @@ const childOfAnotherType: DraggableDimension = getDraggableDimension({ client: fakeArea, }); +const immediate: ScrollOptions = { + shouldPublishImmediately: true, +}; +const scheduled: ScrollOptions = { + shouldPublishImmediately: false, +}; + +const withScrollOptions = (current: State, scrollOptions: ScrollOptions) => { + if (!current.dimension.request) { + throw new Error('Invalid test setup'); + } + + return { + ...current, + dimension: { + ...current.dimension, + request: { + ...current.dimension.request, + scrollOptions, + } + }, + } +}; + describe('dimension marshal', () => { beforeAll(() => { requestAnimationFrame.reset(); @@ -162,7 +188,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); - marshal.onPhaseChange(state.requesting('some-unknown-descriptor')); + marshal.onPhaseChange(state.requesting({ + draggableId: 'some-unknown-descriptor', + scrollOptions: scheduled, + })); expect(callbacks.cancel).toHaveBeenCalled(); }); @@ -181,7 +210,10 @@ describe('dimension marshal', () => { }); // there is now no published home droppable - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); expect(callbacks.cancel).toHaveBeenCalled(); }); @@ -193,7 +225,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1); expect(callbacks.publishDraggable).toBeCalledWith(preset.inHome1); @@ -202,16 +237,40 @@ describe('dimension marshal', () => { expect(callbacks.bulkPublish).not.toHaveBeenCalled(); }); - it('should ask the home droppable to start listening to scrolling', () => { + it('should ask the home droppable to start listening to scrolling (scheduled scroll)', () => { + const callbacks = getCallbackStub(); + const marshal = createDimensionMarshal(callbacks); + const watches = populateMarshal(marshal); + + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); + + // it should not watch scroll on the other droppables at this stage + expect(watches.droppable.watchScroll).toHaveBeenCalledTimes(1); + expect(watches.droppable.watchScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, + scheduled, + ); + }); + + it('should ask the home droppable to start listening to scrolling (immediate scroll)', () => { const callbacks = getCallbackStub(); const marshal = createDimensionMarshal(callbacks); const watches = populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: immediate, + })); // it should not watch scroll on the other droppables at this stage expect(watches.droppable.watchScroll).toHaveBeenCalledTimes(1); - expect(watches.droppable.watchScroll).toHaveBeenCalledWith(preset.home.descriptor.id); + expect(watches.droppable.watchScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, + immediate, + ); }); }); @@ -222,7 +281,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1); expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1); callbacks.publishDraggable.mockReset(); @@ -247,7 +309,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); const watchers = populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1); expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1); expect(callbacks.bulkPublish).not.toHaveBeenCalled(); @@ -272,7 +337,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); const watchers = populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(1); expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(1); watchers.draggable.getDimension.mockClear(); @@ -305,7 +373,10 @@ describe('dimension marshal', () => { droppables, }); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // clearing the initial calls watchers.draggable.getDimension.mockClear(); watchers.droppable.getDimension.mockClear(); @@ -331,7 +402,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); const watchers = populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // called straight away expect(watchers.draggable.getDimension) @@ -353,7 +427,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); const watchers = populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // called straight away expect(watchers.droppable.getDimension) @@ -375,7 +452,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); const watchers = populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // called straight away expect(watchers.droppable.getDimension) @@ -403,7 +483,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // clearing initial calls callbacks.publishDraggable.mockClear(); callbacks.publishDroppable.mockClear(); @@ -427,7 +510,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); @@ -453,7 +539,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); @@ -476,23 +565,32 @@ describe('dimension marshal', () => { }); }); - it('should request all the droppables to start listening to scroll events', () => { - const callbacks = getCallbackStub(); - const marshal = createDimensionMarshal(callbacks); - const watchers = populateMarshal(marshal); - - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - // initial droppable - expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(1); - // clearing this initial call - watchers.droppable.watchScroll.mockClear(); - - marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); - requestAnimationFrame.step(2); - - // excluding the home droppable - const expectedLength: number = Object.keys(preset.droppables).length - 1; - expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(expectedLength); + [scheduled, immediate].forEach((scrollOptions: ScrollOptions) => { + it('should request all the droppables to start listening to scroll events', () => { + const callbacks = getCallbackStub(); + const marshal = createDimensionMarshal(callbacks); + const watchers = populateMarshal(marshal); + + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions, + })); + // initial droppable + expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(1); + + marshal.onPhaseChange(withScrollOptions( + state.dragging(preset.inHome1.descriptor.id), + withScrollOptions, + )); + requestAnimationFrame.step(2); + + const expectedLength: number = Object.keys(preset.droppables).length; + expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(expectedLength); + + Object.keys(preset.droppables).forEach((id: DroppableId) => { + expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(id, scrollOptions); + }); + }); }); it('should not publish dimensions that where not collected', () => { @@ -508,7 +606,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal, { draggables, droppables }); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); @@ -533,7 +634,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal, { draggables, droppables }); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // asserting initial lift occurred expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1); expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home); @@ -560,7 +664,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal, { draggables, droppables }); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // asserting initial lift occurred expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1); expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home); @@ -586,13 +693,16 @@ describe('dimension marshal', () => { const watchers = populateMarshal(marshal); // do initial work - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); // currently only watching Object.keys(preset.droppables).forEach((id: DroppableId) => { - expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(id); + expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(id, scheduled); expect(watchers.droppable.unwatchScroll).not.toHaveBeenCalledWith(id); }); @@ -629,7 +739,7 @@ describe('dimension marshal', () => { watchers = populateMarshal(marshal); }); - const shouldHaveProcessedInitialDimensions = (): void => { + const shouldHaveProcessedInitialDimensions = (scrollOptions: ScrollOptions): void => { expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home); expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1); expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1); @@ -637,8 +747,10 @@ describe('dimension marshal', () => { expect(callbacks.bulkPublish).not.toHaveBeenCalled(); expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(1); expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(1); - expect(watchers.droppable.watchScroll).toHaveBeenCalledWith(preset.home.descriptor.id); expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(1); + expect(watchers.droppable.watchScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, scrollOptions + ); expect(watchers.droppable.unwatchScroll).not.toHaveBeenCalled(); }; @@ -648,91 +760,108 @@ describe('dimension marshal', () => { expect(callbacks.bulkPublish).not.toHaveBeenCalled(); }; - it('should support subsequent drags after a completed collection', () => { - Array.from({ length: 4 }).forEach(() => { - // initial publish - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); - - shouldHaveProcessedInitialDimensions(); - - // resetting mock state so future assertions do not include these calls - resetMocks(); - - // collection and publish of secondary dimensions - marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); - requestAnimationFrame.step(2); + [immediate, scheduled].forEach((scrollOptions: ScrollOptions) => { + it('should support subsequent drags after a completed collection', () => { + Array.from({ length: 4 }).forEach(() => { + // initial publish + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions, + })); + + shouldHaveProcessedInitialDimensions(scrollOptions); + + // resetting mock state so future assertions do not include these calls + resetMocks(); + + // collection and publish of secondary dimensions + marshal.onPhaseChange(withScrollOptions( + state.dragging(preset.inHome1.descriptor.id), + scrollOptions, + )); + requestAnimationFrame.step(2); - expect(callbacks.publishDroppable).not.toHaveBeenCalled(); - expect(callbacks.publishDraggable).not.toHaveBeenCalled(); - expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1); - expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(droppableCount - 1); - expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(droppableCount - 1); - expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(draggableCount - 1); - expect(watchers.droppable.unwatchScroll).not.toHaveBeenCalled(); + expect(callbacks.publishDroppable).not.toHaveBeenCalled(); + expect(callbacks.publishDraggable).not.toHaveBeenCalled(); + expect(callbacks.bulkPublish).toHaveBeenCalledTimes(1); + expect(watchers.droppable.getDimension).toHaveBeenCalledTimes(droppableCount - 1); + expect(watchers.droppable.watchScroll).toHaveBeenCalledTimes(droppableCount - 1); + expect(watchers.draggable.getDimension).toHaveBeenCalledTimes(draggableCount - 1); + expect(watchers.droppable.unwatchScroll).not.toHaveBeenCalled(); - // finish the collection - marshal.onPhaseChange(state.dropComplete(preset.inHome1.descriptor.id)); + // finish the collection + marshal.onPhaseChange(state.dropComplete(preset.inHome1.descriptor.id)); - expect(watchers.droppable.unwatchScroll).toHaveBeenCalledTimes(droppableCount); + expect(watchers.droppable.unwatchScroll).toHaveBeenCalledTimes(droppableCount); - resetMocks(); + resetMocks(); + }); }); - }); - it('should support subsequent drags after a cancelled dimension request', () => { - Array.from({ length: 4 }).forEach(() => { - // start the collection - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + it('should support subsequent drags after a cancelled dimension request', () => { + Array.from({ length: 4 }).forEach(() => { + // start the collection + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions, + })); - shouldHaveProcessedInitialDimensions(); - resetMocks(); + shouldHaveProcessedInitialDimensions(scrollOptions); + resetMocks(); - // cancelled - marshal.onPhaseChange(state.idle); + // cancelled + marshal.onPhaseChange(state.idle); - shouldNotHavePublishedDimensions(); + shouldNotHavePublishedDimensions(); - resetMocks(); + resetMocks(); + }); }); - }); - it('should support subsequent drags after a cancelled drag', () => { - Array.from({ length: 4 }).forEach(() => { - // start the collection - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + it('should support subsequent drags after a cancelled drag', () => { + Array.from({ length: 4 }).forEach(() => { + // start the collection + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions, + })); - shouldHaveProcessedInitialDimensions(); - resetMocks(); + shouldHaveProcessedInitialDimensions(scrollOptions); + resetMocks(); - // drag started but collection not started - marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); + // drag started but collection not started + marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); - shouldNotHavePublishedDimensions(); + shouldNotHavePublishedDimensions(); - // cancelled - marshal.onPhaseChange(state.idle); - resetMocks(); + // cancelled + marshal.onPhaseChange(state.idle); + resetMocks(); + }); }); - }); - it('should support subsequent drags after a cancelled collection', () => { - Array.from({ length: 4 }).forEach(() => { - // start the collection - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + it('should support subsequent drags after a cancelled collection', () => { + Array.from({ length: 4 }).forEach(() => { + // start the collection + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions, + })); - shouldHaveProcessedInitialDimensions(); - resetMocks(); + shouldHaveProcessedInitialDimensions(scrollOptions); + resetMocks(); - // drag started but collection not started - marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); - // executing collection step but not publish - requestAnimationFrame.step(); + // drag started but collection not started + marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); + // executing collection step but not publish + requestAnimationFrame.step(); - shouldNotHavePublishedDimensions(); + shouldNotHavePublishedDimensions(); - // cancelled - marshal.onPhaseChange(state.idle); - resetMocks(); + // cancelled + marshal.onPhaseChange(state.idle); + resetMocks(); + }); }); }); }); @@ -757,7 +886,10 @@ describe('dimension marshal', () => { marshal.registerDroppable(preset.home.descriptor, droppableCallbacks); marshal.registerDraggable(preset.inHome1.descriptor, getDraggableDimensionFn); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.home); }); @@ -782,7 +914,10 @@ describe('dimension marshal', () => { }; marshal.registerDroppable(newHomeDescriptor, newCallbacks); marshal.registerDraggable(preset.inHome1.descriptor, getDraggableDimensionFn); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); expect(callbacks.publishDroppable).toHaveBeenCalledWith(preset.foreign); expect(callbacks.publishDroppable).toHaveBeenCalledTimes(1); @@ -806,7 +941,10 @@ describe('dimension marshal', () => { marshal.registerDroppable(preset.home.descriptor, droppableCallbacks); marshal.registerDraggable(preset.inHome1.descriptor, getDraggableDimensionFn); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome1); }); @@ -829,7 +967,10 @@ describe('dimension marshal', () => { index: preset.inHome1.descriptor.index + 10, }; marshal.registerDraggable(fake, () => preset.inHome2); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); expect(callbacks.publishDraggable).toHaveBeenCalledTimes(1); expect(callbacks.publishDraggable).toHaveBeenCalledWith(preset.inHome2); @@ -874,7 +1015,10 @@ describe('dimension marshal', () => { expect(console.warn).not.toHaveBeenCalled(); // lift, collect and publish - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // execute full lift marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); @@ -903,7 +1047,10 @@ describe('dimension marshal', () => { // not unregistering children (bad) // lift, collect and publish - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // perform full lift marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); @@ -951,7 +1098,10 @@ describe('dimension marshal', () => { }); // perform full lift - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); @@ -976,7 +1126,10 @@ describe('dimension marshal', () => { marshal.unregisterDraggable(preset.inForeign1.descriptor); expect(console.error).not.toHaveBeenCalled(); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); // perform full lift marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); @@ -1020,7 +1173,10 @@ describe('dimension marshal', () => { marshal.unregisterDraggable(preset.inHome2.descriptor); // perform full lift - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); marshal.onPhaseChange(state.dragging(preset.inHome1.descriptor.id)); requestAnimationFrame.step(2); @@ -1049,7 +1205,10 @@ describe('dimension marshal', () => { }); // start a collection - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); callbacks.publishDraggable.mockReset(); // now registering marshal.registerDraggable(fake.descriptor, () => fake); @@ -1079,7 +1238,10 @@ describe('dimension marshal', () => { }; // starting collection - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); callbacks.publishDroppable.mockReset(); // updating registration @@ -1121,7 +1283,7 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting()); marshal.updateDroppableScroll(preset.home.descriptor.id, { x: 100, y: 230 }); expect(callbacks.updateDroppableScroll) @@ -1158,7 +1320,10 @@ describe('dimension marshal', () => { const marshal = createDimensionMarshal(callbacks); populateMarshal(marshal); - marshal.onPhaseChange(state.requesting(preset.inHome1.descriptor.id)); + marshal.onPhaseChange(state.requesting({ + draggableId: preset.inHome1.descriptor.id, + scrollOptions: scheduled, + })); marshal.updateDroppableIsEnabled(preset.home.descriptor.id, false); expect(callbacks.updateDroppableIsEnabled) diff --git a/test/utils/get-simple-state-preset.js b/test/utils/get-simple-state-preset.js index 5e5356d196..65349005ea 100644 --- a/test/utils/get-simple-state-preset.js +++ b/test/utils/get-simple-state-preset.js @@ -12,6 +12,7 @@ import type { DroppableDimension, CurrentDragPositions, InitialDragPositions, + InitialLiftRequest, Position, DragState, DropResult, @@ -19,13 +20,18 @@ import type { DropReason, DraggableId, DragImpact, + ScrollOptions, } from '../../src/types'; +const scheduled: ScrollOptions = { + shouldPublishImmediately: false, +}; + export default (axis?: Axis = vertical) => { const preset = getPreset(axis); - const getDimensionState = (request: DraggableId): DimensionState => { - const draggable: DraggableDimension = preset.draggables[request]; + const getDimensionState = (request: InitialLiftRequest): DimensionState => { + const draggable: DraggableDimension = preset.draggables[request.draggableId]; const home: DroppableDimension = preset.droppables[draggable.descriptor.droppableId]; const result: DimensionState = { @@ -52,7 +58,13 @@ export default (axis?: Axis = vertical) => { phase: 'PREPARING', }; - const requesting = (request?: DraggableId = preset.inHome1.descriptor.id): State => { + const defaultLiftRequest: InitialLiftRequest = { + draggableId: preset.inHome1.descriptor.id, + scrollOptions: { + shouldPublishImmediately: false, + }, + }; + const requesting = (request?: InitialLiftRequest = defaultLiftRequest): State => { const result: State = { phase: 'COLLECTING_INITIAL_DIMENSIONS', drag: null, @@ -109,7 +121,10 @@ export default (axis?: Axis = vertical) => { phase: 'DRAGGING', drag, drop: null, - dimension: getDimensionState(id), + dimension: getDimensionState({ + draggableId: id, + scrollOptions: scheduled, + }), }; return result; @@ -166,7 +181,10 @@ export default (axis?: Axis = vertical) => { phase: 'DRAGGING', drag, drop: null, - dimension: getDimensionState(id), + dimension: getDimensionState({ + draggableId: id, + scrollOptions: scheduled, + }), }; return result; @@ -197,7 +215,10 @@ export default (axis?: Axis = vertical) => { pending, result: null, }, - dimension: getDimensionState(descriptor.id), + dimension: getDimensionState({ + draggableId: descriptor.id, + scrollOptions: scheduled, + }), }; return result; }; From 249f6cb0aab3a66085b260fd44755bd64b93a4be Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 15:13:38 +1100 Subject: [PATCH 147/163] more tests --- docs/guides/screen-reader.md | 2 +- src/state/action-creators.js | 8 +- .../dimension-marshal/dimension-marshal.js | 31 ++-- src/state/reducer.js | 5 +- src/types.js | 4 +- test/unit/state/action-creators.spec.js | 4 +- .../visibility/is-partially-visible.spec.js | 169 ++++++++++-------- test/unit/view/dimension-marshal.spec.js | 7 +- test/utils/get-simple-state-preset.js | 8 +- 9 files changed, 136 insertions(+), 102 deletions(-) diff --git a/docs/guides/screen-reader.md b/docs/guides/screen-reader.md index feb98691e5..30cbccab96 100644 --- a/docs/guides/screen-reader.md +++ b/docs/guides/screen-reader.md @@ -12,7 +12,7 @@ For the default messages we have gone for a friendly tone. We have also chosen t ## `HookProvided` > `Announce` -The `announce` function that is provided to each of the `Hook` functions can be used to provide your own screen reader message. This message will be immediately read out. In order to provide a fast and responsive experience to users **you must provide this message sycnously**. If you attempt to hold onto the `announce` function and call it later it will not work and will just print a warning to the console. Additionally, if you try to call `announce` twice for the same event then only the first will be read by the screen reader with subsequent calls to `announce` being ignored and a warning printed. +The `announce` function that is provided to each of the `Hook` functions can be used to provide your own screen reader message. This message will be immediately read out. In order to provide a fast and responsive experience to users **you must provide this message synchronously**. If you attempt to hold onto the `announce` function and call it later it will not work and will just print a warning to the console. Additionally, if you try to call `announce` twice for the same event then only the first will be read by the screen reader with subsequent calls to `announce` being ignored and a warning printed. ## Step 1: instructions diff --git a/src/state/action-creators.js b/src/state/action-creators.js index e7893c6717..f38c92139c 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -14,7 +14,7 @@ import type { CurrentDrag, InitialDrag, DraggableDescriptor, - InitialLiftRequest, + LiftRequest, AutoScrollMode, ScrollOptions, } from '../types'; @@ -50,10 +50,10 @@ const getScrollDiff = ({ export type RequestDimensionsAction = {| type: 'REQUEST_DIMENSIONS', - payload: InitialLiftRequest, + payload: LiftRequest, |} -export const requestDimensions = (request: InitialLiftRequest): RequestDimensionsAction => ({ +export const requestDimensions = (request: LiftRequest): RequestDimensionsAction => ({ type: 'REQUEST_DIMENSIONS', payload: request, }); @@ -495,7 +495,7 @@ export const lift = (id: DraggableId, const scrollOptions: ScrollOptions = { shouldPublishImmediately: autoScrollMode === 'JUMP', }; - const request: InitialLiftRequest = { + const request: LiftRequest = { draggableId: id, scrollOptions, }; diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index 1d87a1f015..0065ede53a 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -9,7 +9,7 @@ import type { State as AppState, Phase, Position, - InitialLiftRequest, + LiftRequest, ScrollOptions, } from '../../types'; import type { @@ -32,7 +32,7 @@ type State = {| // short lived isCollecting: boolean, scrollOptions: ?ScrollOptions, - request: ?InitialLiftRequest, + request: ?LiftRequest, frameId: ?number, |} @@ -242,7 +242,7 @@ export default (callbacks: Callbacks) => { const getToBeCollected = (): UnknownDescriptorType[] => { const draggables: DraggableEntryMap = state.draggables; const droppables: DroppableEntryMap = state.droppables; - const request: ?InitialLiftRequest = state.request; + const request: ?LiftRequest = state.request; if (!request) { console.error('cannot find request in state'); @@ -290,7 +290,7 @@ export default (callbacks: Callbacks) => { return toBeCollected; }; - const processPrimaryDimensions = (request: ?InitialLiftRequest) => { + const processPrimaryDimensions = (request: ?LiftRequest) => { if (state.isCollecting) { cancel('Cannot start capturing dimensions for a drag it is already dragging'); return; @@ -343,16 +343,26 @@ export default (callbacks: Callbacks) => { }); }; - const processSecondaryDimensions = (): void => { + const processSecondaryDimensions = (requestInAppState: ?LiftRequest): void => { if (!state.isCollecting) { cancel('Cannot collect secondary dimensions when collection is not occurring'); return; } - const request: ?InitialLiftRequest = state.request; + const request: ?LiftRequest = state.request; if (!request) { - console.error('Cannot process secondary dimensions without a request'); + cancel('Cannot process secondary dimensions without a request'); + return; + } + + if (!requestInAppState) { + cancel('Cannot process secondary dimensions without a request on the state'); + return; + } + + if (requestInAppState.draggableId !== request.draggableId) { + cancel('Cannot process secondary dimensions as local request does not match app state'); return; } @@ -432,12 +442,7 @@ export default (callbacks: Callbacks) => { } if (phase === 'DRAGGING') { - if (current.dimension.request.draggableId !== state.request.draggableId) { - cancel('Request in local state does not match that of the store'); - return; - } - - processSecondaryDimensions(); + processSecondaryDimensions(current.dimension.request); return; } diff --git a/src/state/reducer.js b/src/state/reducer.js index 9e7e74fd10..ebee9737b9 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -22,7 +22,7 @@ import type { CurrentDragPositions, Position, InitialDragPositions, - InitialLiftRequest, + LiftRequest, } from '../types'; import { add, subtract, isEqual } from './position'; import { noMovement } from './no-impact'; @@ -59,6 +59,7 @@ type MoveArgs = {| scrollJumpRequest?: ?Position, |} +// TODO: use map const canPublishDimension = (phase: Phase): boolean => ['IDLE', 'DROP_ANIMATING', 'DROP_COMPLETE'].indexOf(phase) === -1; @@ -174,7 +175,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } - const request: InitialLiftRequest = action.payload; + const request: LiftRequest = action.payload; return { phase: 'COLLECTING_INITIAL_DIMENSIONS', diff --git a/src/types.js b/src/types.js index af087f3392..1f4e4ad3f8 100644 --- a/src/types.js +++ b/src/types.js @@ -293,7 +293,7 @@ export type ScrollOptions = {| shouldPublishImmediately: boolean, |} -export type InitialLiftRequest = {| +export type LiftRequest = {| draggableId: DraggableId, scrollOptions: ScrollOptions, |} @@ -304,7 +304,7 @@ export type DimensionState = {| // descriptor may not be the same as the actual descriptor. To avoid // confusion the request is just an id which is looked up // in the dimension-marshal post-flush - request: ?InitialLiftRequest, + request: ?LiftRequest, draggable: DraggableDimensionMap, droppable: DroppableDimensionMap, |}; diff --git a/test/unit/state/action-creators.spec.js b/test/unit/state/action-creators.spec.js index 4167d0c08c..c6c19586f4 100644 --- a/test/unit/state/action-creators.spec.js +++ b/test/unit/state/action-creators.spec.js @@ -19,7 +19,7 @@ import type { DraggableId, Store, InitialDragPositions, - InitialLiftRequest, + LiftRequest, } from '../../../src/types'; const preset = getPreset(); @@ -77,7 +77,7 @@ describe('action creators', () => { // Phase 2: request dimensions after flushing animations jest.runOnlyPendingTimers(); - const request: InitialLiftRequest = { + const request: LiftRequest = { draggableId: preset.inHome1.descriptor.id, scrollOptions: { shouldPublishImmediately: false, diff --git a/test/unit/state/visibility/is-partially-visible.spec.js b/test/unit/state/visibility/is-partially-visible.spec.js index ef21196282..fea9ce6f47 100644 --- a/test/unit/state/visibility/is-partially-visible.spec.js +++ b/test/unit/state/visibility/is-partially-visible.spec.js @@ -2,7 +2,8 @@ import getArea from '../../../../src/state/get-area'; import { isPartiallyVisible } from '../../../../src/state/visibility/is-visible'; import { getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; -import { offset } from '../../../../src/state/spacing'; +import { offsetByPosition } from '../../../../src/state/spacing'; +import { getClosestScrollable } from '../../../utils/dimension'; import type { Area, DroppableDimension, @@ -45,7 +46,7 @@ const notInViewport: Spacing = { bottom: 600, }; -const smallDroppable: DroppableDimension = getDroppableDimension({ +const asBigAsInViewport1: DroppableDimension = getDroppableDimension({ descriptor: { id: 'subset', type: 'TYPE', @@ -83,13 +84,13 @@ describe('is partially visible', () => { it('should return true if the item is partially visible in the viewport', () => { const partials: Spacing[] = [ // bleed over top - offset(viewport, { x: 0, y: -1 }), + offsetByPosition(viewport, { x: 0, y: -1 }), // bleed over right - offset(viewport, { x: 1, y: 0 }), + offsetByPosition(viewport, { x: 1, y: 0 }), // bleed over bottom - offset(viewport, { x: 0, y: 1 }), + offsetByPosition(viewport, { x: 0, y: 1 }), // bleed over left - offset(viewport, { x: -1, y: 0 }), + offsetByPosition(viewport, { x: -1, y: 0 }), ]; partials.forEach((partial: Spacing) => { @@ -115,6 +116,13 @@ describe('is partially visible', () => { left: viewport.left, right: viewport.right, }), + closest: { + frameClient: viewport, + scrollWidth: viewport.width, + scrollHeight: viewport.bottom + 100, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, }); describe('originally invisible but now invisible', () => { @@ -170,12 +178,40 @@ describe('is partially visible', () => { }); describe('droppable', () => { + const client: Area = getArea({ + top: 0, + left: 0, + right: 600, + bottom: 600, + }); + const frame: Area = getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }); + + const scrollable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'clipped', + type: 'TYPE', + }, + client, + closest: { + frameClient: frame, + scrollHeight: client.height, + scrollWidth: client.width, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + describe('without changes in droppable scroll', () => { it('should return false if outside the droppable', () => { expect(isPartiallyVisible({ target: inViewport2, viewport, - destination: smallDroppable, + destination: asBigAsInViewport1, })).toBe(false); }); @@ -183,7 +219,7 @@ describe('is partially visible', () => { expect(isPartiallyVisible({ target: viewport, viewport, - destination: smallDroppable, + destination: asBigAsInViewport1, })).toBe(true); }); @@ -191,7 +227,7 @@ describe('is partially visible', () => { expect(isPartiallyVisible({ target: inViewport1, viewport, - destination: smallDroppable, + destination: asBigAsInViewport1, })).toBe(true); }); @@ -206,33 +242,33 @@ describe('is partially visible', () => { expect(isPartiallyVisible({ target: insideDroppable, viewport, - destination: smallDroppable, + destination: asBigAsInViewport1, })).toBe(true); }); it('should return true if partially within the droppable', () => { const partials: Spacing[] = [ // bleed over top - offset(inViewport1, { x: 0, y: -1 }), + offsetByPosition(inViewport1, { x: 0, y: -1 }), // bleed over right - offset(inViewport1, { x: 1, y: 0 }), + offsetByPosition(inViewport1, { x: 1, y: 0 }), // bleed over bottom - offset(inViewport1, { x: 0, y: 1 }), + offsetByPosition(inViewport1, { x: 0, y: 1 }), // bleed over left - offset(inViewport1, { x: -1, y: 0 }), + offsetByPosition(inViewport1, { x: -1, y: 0 }), ]; partials.forEach((partial: Spacing) => { expect(isPartiallyVisible({ target: partial, viewport, - destination: smallDroppable, + destination: asBigAsInViewport1, })).toBe(true); }); }); it('should return false if falling on clipped area of droppable', () => { - const frame: Spacing = { + const ourFrame: Spacing = { top: 10, left: 10, right: 100, @@ -245,14 +281,14 @@ describe('is partially visible', () => { type: 'TYPE', }, client: getArea({ - ...frame, + ...ourFrame, // stretches out past frame bottom: 600, }), closest: { - frameClient: getArea(frame), + frameClient: getArea(ourFrame), scrollHeight: 600, - scrollWidth: getArea(frame).width, + scrollWidth: getArea(ourFrame).width, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -272,45 +308,26 @@ describe('is partially visible', () => { }); describe('with changes in droppable scroll', () => { - const frame: Spacing = { - top: 10, - left: 10, - right: 100, - // cuts the droppable short - bottom: 100, - }; - const clippedDroppable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'clipped', - type: 'TYPE', - }, - client: getArea({ - ...frame, - // stretches out past frame - bottom: 600, - }), - frameClient: getArea(frame), - }); - describe('originally invisible but now invisible', () => { it('should take into account the droppable scroll when detecting visibility', () => { const originallyInvisible: Spacing = { - ...frame, - top: 110, - bottom: 200, + left: client.left, + right: client.right, + top: frame.bottom + 10, + bottom: frame.bottom + 20, }; // originally invisible expect(isPartiallyVisible({ target: originallyInvisible, - destination: clippedDroppable, + destination: scrollable, viewport, })).toBe(false); // after scroll the target is now visible expect(isPartiallyVisible({ target: originallyInvisible, - destination: scrollDroppable(clippedDroppable, { x: 0, y: 100 }), + destination: scrollDroppable(scrollable, { x: 0, y: 100 }), viewport, })).toBe(true); }); @@ -327,14 +344,14 @@ describe('is partially visible', () => { // originally visible expect(isPartiallyVisible({ target: originallyVisible, - destination: clippedDroppable, + destination: scrollable, viewport, })).toBe(true); // after scroll the target is now invisible expect(isPartiallyVisible({ target: originallyVisible, - destination: scrollDroppable(clippedDroppable, { x: 0, y: 100 }), + destination: scrollDroppable(scrollable, { x: 0, y: 100 }), viewport, })).toBe(false); }); @@ -342,26 +359,33 @@ describe('is partially visible', () => { }); describe('with invisible subject', () => { - const frame: Spacing = { - top: 10, - left: 10, - right: 100, - bottom: 600, - }; - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'clipped', - type: 'TYPE', - }, - client: getArea({ - ...frame, - // smaller than frame - bottom: 100, - }), - frameClient: getArea(frame), - }); - it('should return false when subject is totally invisible', () => { + // creating a droppable where the frame is bigger than the subject + const droppable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'droppable', + type: 'TYPE', + }, + client: getArea({ + top: 0, + left: 0, + bottom: 100, + right: 100, + }), + closest: { + frameClient: getArea({ + top: 0, + left: 0, + bottom: 100, + right: 100, + }), + scrollHeight: 600, + scrollWidth: 600, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const originallyVisible: Spacing = { ...frame, top: 10, @@ -376,14 +400,19 @@ describe('is partially visible', () => { })).toBe(true); // subject is now totally invisible - const scrolled: DroppableDimension = scrollDroppable(droppable, { x: 0, y: 101 }); + const scrolled: DroppableDimension = scrollDroppable( + droppable, + getClosestScrollable(droppable).scroll.max, + ); + // asserting frame is not visible + expect(scrolled.viewport.clipped).toBe(null); + + // now asserting that this check will fail expect(isPartiallyVisible({ target: originallyVisible, destination: scrolled, viewport, })).toBe(false); - // asserting frame is not visible - expect(scrolled.viewport.clipped).toBe(null); }); }); }); @@ -393,7 +422,7 @@ describe('is partially visible', () => { expect(isPartiallyVisible({ target: inViewport1, viewport, - destination: smallDroppable, + destination: asBigAsInViewport1, })).toBe(true); }); @@ -401,7 +430,7 @@ describe('is partially visible', () => { expect(isPartiallyVisible({ target: inViewport2, viewport, - destination: smallDroppable, + destination: asBigAsInViewport1, })).toBe(false); }); diff --git a/test/unit/view/dimension-marshal.spec.js b/test/unit/view/dimension-marshal.spec.js index ce4dba1d60..8266a90ca1 100644 --- a/test/unit/view/dimension-marshal.spec.js +++ b/test/unit/view/dimension-marshal.spec.js @@ -21,7 +21,6 @@ import type { DraggableDescriptor, DroppableDescriptor, ScrollOptions, - InitialLiftRequest, Area, } from '../../../src/types'; @@ -147,9 +146,9 @@ const withScrollOptions = (current: State, scrollOptions: ScrollOptions) => { request: { ...current.dimension.request, scrollOptions, - } + }, }, - } + }; }; describe('dimension marshal', () => { @@ -580,7 +579,7 @@ describe('dimension marshal', () => { marshal.onPhaseChange(withScrollOptions( state.dragging(preset.inHome1.descriptor.id), - withScrollOptions, + scrollOptions, )); requestAnimationFrame.step(2); diff --git a/test/utils/get-simple-state-preset.js b/test/utils/get-simple-state-preset.js index 65349005ea..85b5fb8d1d 100644 --- a/test/utils/get-simple-state-preset.js +++ b/test/utils/get-simple-state-preset.js @@ -12,7 +12,7 @@ import type { DroppableDimension, CurrentDragPositions, InitialDragPositions, - InitialLiftRequest, + LiftRequest, Position, DragState, DropResult, @@ -30,7 +30,7 @@ const scheduled: ScrollOptions = { export default (axis?: Axis = vertical) => { const preset = getPreset(axis); - const getDimensionState = (request: InitialLiftRequest): DimensionState => { + const getDimensionState = (request: LiftRequest): DimensionState => { const draggable: DraggableDimension = preset.draggables[request.draggableId]; const home: DroppableDimension = preset.droppables[draggable.descriptor.droppableId]; @@ -58,13 +58,13 @@ export default (axis?: Axis = vertical) => { phase: 'PREPARING', }; - const defaultLiftRequest: InitialLiftRequest = { + const defaultLiftRequest: LiftRequest = { draggableId: preset.inHome1.descriptor.id, scrollOptions: { shouldPublishImmediately: false, }, }; - const requesting = (request?: InitialLiftRequest = defaultLiftRequest): State => { + const requesting = (request?: LiftRequest = defaultLiftRequest): State => { const result: State = { phase: 'COLLECTING_INITIAL_DIMENSIONS', drag: null, From 2c8d1e2ec0f07c1e96fec63481c85dfdeab95f0b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 16:17:11 +1100 Subject: [PATCH 148/163] more tests. more green --- src/state/dimension.js | 2 +- .../get-forced-displacement.js | 1 - src/state/move-to-next-index/in-home-list.js | 2 +- test/unit/state/dimension.spec.js | 1 + test/unit/state/move-to-next-index.spec.js | 3 - ...s-partially-visible-through-frame.spec.js} | 64 +-- .../visibility/is-partially-visible.spec.js | 4 +- .../is-totally-visible-through-frame.spec.js | 82 +++ .../visibility/is-totally-visible.spec.js | 470 ++++++++++++++++++ test/unit/view/unconnected-droppable.spec.js | 13 +- test/utils/get-simple-state-preset.js | 5 +- 11 files changed, 591 insertions(+), 56 deletions(-) rename test/unit/state/visibility/{is-visible-through-frame.spec.js => is-partially-visible-through-frame.spec.js} (60%) create mode 100644 test/unit/state/visibility/is-totally-visible-through-frame.spec.js create mode 100644 test/unit/state/visibility/is-totally-visible.spec.js diff --git a/src/state/dimension.js b/src/state/dimension.js index dbc9991749..38573f2543 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -2,7 +2,7 @@ import { vertical, horizontal } from './axis'; import getArea from './get-area'; import { offsetByPosition, expandBySpacing } from './spacing'; -import { subtract, negate, isEqual } from './position'; +import { subtract, negate } from './position'; import getMaxScroll from './get-max-scroll'; import type { DraggableDescriptor, diff --git a/src/state/move-to-next-index/get-forced-displacement.js b/src/state/move-to-next-index/get-forced-displacement.js index d3a38408b4..604fa70995 100644 --- a/src/state/move-to-next-index/get-forced-displacement.js +++ b/src/state/move-to-next-index/get-forced-displacement.js @@ -3,7 +3,6 @@ import getDisplacement from '../get-displacement'; import type { Area, Axis, - Position, DraggableId, DragImpact, DraggableDimensionMap, diff --git a/src/state/move-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js index 0b881b4d30..44139d59b0 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,6 +1,6 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import { patch, subtract, absolute } from '../position'; +import { patch, subtract } from '../position'; import withDroppableDisplacement from '../with-droppable-displacement'; import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; import getViewport from '../../window/get-viewport'; diff --git a/test/unit/state/dimension.spec.js b/test/unit/state/dimension.spec.js index cd43bc400d..b7ce15f5df 100644 --- a/test/unit/state/dimension.spec.js +++ b/test/unit/state/dimension.spec.js @@ -219,6 +219,7 @@ describe('dimension', () => { describe('closest scrollable', () => { describe('basic info about the scrollable', () => { + // eslint-disable-next-line no-shadow const client: Area = getArea({ top: 0, right: 300, diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index 59713f67fb..eb4a62bd2d 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -9,9 +9,7 @@ import { vertical, horizontal } from '../../../src/state/axis'; import { isPartiallyVisible } from '../../../src/state/visibility/is-visible'; import getViewport from '../../../src/window/get-viewport'; import getArea from '../../../src/state/get-area'; -import setWindowScroll from '../../utils/set-window-scroll'; import { getDroppableDimension, getDraggableDimension, scrollDroppable } from '../../../src/state/dimension'; -import getMaxScroll from '../../../src/state/get-max-scroll'; import type { Area, Axis, @@ -21,7 +19,6 @@ import type { DroppableDimension, DraggableLocation, Position, - Displacement, } from '../../../src/types'; const setViewport = (custom: Area): void => { diff --git a/test/unit/state/visibility/is-visible-through-frame.spec.js b/test/unit/state/visibility/is-partially-visible-through-frame.spec.js similarity index 60% rename from test/unit/state/visibility/is-visible-through-frame.spec.js rename to test/unit/state/visibility/is-partially-visible-through-frame.spec.js index 7e0d3ccdec..92199ee713 100644 --- a/test/unit/state/visibility/is-visible-through-frame.spec.js +++ b/test/unit/state/visibility/is-partially-visible-through-frame.spec.js @@ -1,29 +1,29 @@ // @flow -import isVisibleThroughFrame from '../../../../src/state/visibility/is-visible-through-frame'; -import { add, offset } from '../../../../src/state/spacing'; +import isPartiallyVisibleThroughFrame from '../../../../src/state/visibility/is-partially-visible-through-frame'; +import { offsetByPosition, expandBySpacing } from '../../../../src/state/spacing'; import type { Spacing } from '../../../../src/types'; const frame: Spacing = { top: 0, left: 0, right: 100, bottom: 100, }; -describe('is visible through frame', () => { +describe('is partially visible through frame', () => { describe('subject is smaller than frame', () => { describe('completely outside frame', () => { it('should return false if subject is outside frame on any side', () => { const outside: Spacing[] = [ // outside on top - offset(frame, { x: 0, y: -101 }), + offsetByPosition(frame, { x: 0, y: -101 }), // outside on right - offset(frame, { x: 101, y: 0 }), + offsetByPosition(frame, { x: 101, y: 0 }), // outside on bottom - offset(frame, { x: 0, y: 101 }), + offsetByPosition(frame, { x: 0, y: 101 }), // outside on left - offset(frame, { x: -101, y: 0 }), + offsetByPosition(frame, { x: -101, y: 0 }), ]; outside.forEach((subject: Spacing) => { - expect(isVisibleThroughFrame(frame)(subject)).toBe(false); + expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(false); }); }); }); @@ -37,7 +37,7 @@ describe('is visible through frame', () => { bottom: 90, }; - expect(isVisibleThroughFrame(frame)(subject)).toBe(true); + expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(true); }); }); @@ -54,55 +54,29 @@ describe('is visible through frame', () => { right: 150, }; - expect(isVisibleThroughFrame(frame)(subject)).toBe(true); - }); - - it('should return false when only partially visible horizontally', () => { - const subject: Spacing = { - // visible - left: 50, - // not visible - top: 110, - bottom: 150, - right: 150, - }; - - expect(isVisibleThroughFrame(frame)(subject)).toBe(false); - }); - - it('should return false when only partially visible vertically', () => { - const subject: Spacing = { - // visible - top: 10, - // not visible - bottom: 110, - left: 110, - right: 150, - }; - - expect(isVisibleThroughFrame(frame)(subject)).toBe(false); + expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(true); }); }); }); describe('subject is equal to frame', () => { it('should return true when the frame is equal to the subject', () => { - expect(isVisibleThroughFrame(frame)(frame)).toBe(true); - expect(isVisibleThroughFrame(frame)({ ...frame })).toBe(true); + expect(isPartiallyVisibleThroughFrame(frame)(frame)).toBe(true); + expect(isPartiallyVisibleThroughFrame(frame)({ ...frame })).toBe(true); }); }); describe('subject is bigger than frame', () => { - const bigSubject: Spacing = add(frame, frame); + const bigSubject: Spacing = expandBySpacing(frame, frame); it('should return false if the subject has no overlap with the frame', () => { - const subject: Spacing = offset(bigSubject, { x: 1000, y: 1000 }); + const subject: Spacing = offsetByPosition(bigSubject, { x: 1000, y: 1000 }); - expect(isVisibleThroughFrame(frame)(subject)).toBe(false); + expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(false); }); it('should return true if subject is bigger on every side', () => { - expect(isVisibleThroughFrame(frame)(bigSubject)).toBe(true); + expect(isPartiallyVisibleThroughFrame(frame)(bigSubject)).toBe(true); }); describe('partially visible', () => { @@ -120,7 +94,7 @@ describe('is visible through frame', () => { ]; subjects.forEach((subject: Spacing) => { - expect(isVisibleThroughFrame(frame)(subject)).toBe(true); + expect(isPartiallyVisibleThroughFrame(frame)(subject)).toBe(true); }); }); @@ -134,7 +108,7 @@ describe('is visible through frame', () => { right: 500, }; - expect(isVisibleThroughFrame(frame)(subject)).toEqual(false); + expect(isPartiallyVisibleThroughFrame(frame)(subject)).toEqual(false); }); it('should return false when only partially visible vertically', () => { @@ -147,7 +121,7 @@ describe('is visible through frame', () => { right: 500, }; - expect(isVisibleThroughFrame(frame)(subject)).toEqual(false); + expect(isPartiallyVisibleThroughFrame(frame)(subject)).toEqual(false); }); }); }); diff --git a/test/unit/state/visibility/is-partially-visible.spec.js b/test/unit/state/visibility/is-partially-visible.spec.js index fea9ce6f47..daad3296aa 100644 --- a/test/unit/state/visibility/is-partially-visible.spec.js +++ b/test/unit/state/visibility/is-partially-visible.spec.js @@ -311,8 +311,8 @@ describe('is partially visible', () => { describe('originally invisible but now invisible', () => { it('should take into account the droppable scroll when detecting visibility', () => { const originallyInvisible: Spacing = { - left: client.left, - right: client.right, + left: frame.left, + right: frame.right, top: frame.bottom + 10, bottom: frame.bottom + 20, }; diff --git a/test/unit/state/visibility/is-totally-visible-through-frame.spec.js b/test/unit/state/visibility/is-totally-visible-through-frame.spec.js new file mode 100644 index 0000000000..d6621bbcd0 --- /dev/null +++ b/test/unit/state/visibility/is-totally-visible-through-frame.spec.js @@ -0,0 +1,82 @@ +// @flow +import isTotallyVisibleThroughFrame from '../../../../src/state/visibility/is-totally-visible-through-frame'; +import { offsetByPosition, expandBySpacing } from '../../../../src/state/spacing'; +import type { Spacing } from '../../../../src/types'; + +const frame: Spacing = { + top: 0, left: 0, right: 100, bottom: 100, +}; + +describe('is totally visible through frame', () => { + describe('subject is smaller than frame', () => { + describe('completely outside frame', () => { + it('should return false if subject is outside frame on any side', () => { + const outside: Spacing[] = [ + // outside on top + offsetByPosition(frame, { x: 0, y: -101 }), + // outside on right + offsetByPosition(frame, { x: 101, y: 0 }), + // outside on bottom + offsetByPosition(frame, { x: 0, y: 101 }), + // outside on left + offsetByPosition(frame, { x: -101, y: 0 }), + ]; + + outside.forEach((subject: Spacing) => { + expect(isTotallyVisibleThroughFrame(frame)(subject)).toBe(false); + }); + }); + }); + + describe('contained in frame', () => { + it('should return true when subject is contained within frame', () => { + const subject: Spacing = { + top: 10, + left: 10, + right: 90, + bottom: 90, + }; + + expect(isTotallyVisibleThroughFrame(frame)(subject)).toBe(true); + }); + }); + + describe('partially visible', () => { + it('should return false if partially visible horizontally and vertically', () => { + const subject: Spacing = { + // visible + top: 10, + // not visible + bottom: 110, + // visible + left: 50, + // not visible + right: 150, + }; + + expect(isTotallyVisibleThroughFrame(frame)(subject)).toBe(false); + }); + }); + }); + + describe('subject is equal to frame', () => { + it('should return true when the frame is equal to the subject', () => { + expect(isTotallyVisibleThroughFrame(frame)(frame)).toBe(true); + expect(isTotallyVisibleThroughFrame(frame)({ ...frame })).toBe(true); + }); + }); + + describe('subject is bigger than frame', () => { + const bigSubject: Spacing = expandBySpacing(frame, frame); + + it('should return false if the subject has no overlap with the frame', () => { + const subject: Spacing = offsetByPosition(bigSubject, { x: 1000, y: 1000 }); + + expect(isTotallyVisibleThroughFrame(frame)(subject)).toBe(false); + }); + + it('should return false if subject is bigger on every side', () => { + expect(isTotallyVisibleThroughFrame(frame)(bigSubject)).toBe(false); + }); + }); +}); diff --git a/test/unit/state/visibility/is-totally-visible.spec.js b/test/unit/state/visibility/is-totally-visible.spec.js new file mode 100644 index 0000000000..6c64411121 --- /dev/null +++ b/test/unit/state/visibility/is-totally-visible.spec.js @@ -0,0 +1,470 @@ +// @flow +import getArea from '../../../../src/state/get-area'; +import { isTotallyVisible, isPartiallyVisible } from '../../../../src/state/visibility/is-visible'; +import { getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; +import { offsetByPosition } from '../../../../src/state/spacing'; +import { getClosestScrollable } from '../../../utils/dimension'; +import type { + Area, + DroppableDimension, + Spacing, +} from '../../../../src/types'; + +const viewport: Area = getArea({ + right: 800, + top: 0, + left: 0, + bottom: 600, +}); + +const asBigAsViewport: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'same-as-viewport', + type: 'TYPE', + }, + client: viewport, +}); + +const inViewport1: Spacing = { + top: 10, + left: 10, + right: 100, + bottom: 100, +}; + +const inViewport2: Spacing = { + top: 10, + left: 200, + right: 400, + bottom: 100, +}; + +const notInViewport: Spacing = { + top: 0, + right: 1000, + left: 900, + bottom: 600, +}; + +const asBigAsInViewport1: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'subset', + type: 'TYPE', + }, + client: getArea(inViewport1), +}); + +describe('is totally visible', () => { + describe('viewport', () => { + describe('without changes in droppable scroll', () => { + it('should return false if the item is not in the viewport', () => { + expect(isTotallyVisible({ + target: notInViewport, + viewport, + destination: asBigAsViewport, + })).toBe(false); + }); + + it('should return true if item takes up entire viewport', () => { + expect(isTotallyVisible({ + target: viewport, + viewport, + destination: asBigAsViewport, + })).toBe(true); + }); + + it('should return true if the item is totally visible in the viewport', () => { + expect(isTotallyVisible({ + target: inViewport1, + viewport, + destination: asBigAsViewport, + })).toBe(true); + }); + + it('should return false if the item is partially visible in the viewport', () => { + const partials: Spacing[] = [ + // bleed over top + offsetByPosition(viewport, { x: 0, y: -1 }), + // bleed over right + offsetByPosition(viewport, { x: 1, y: 0 }), + // bleed over bottom + offsetByPosition(viewport, { x: 0, y: 1 }), + // bleed over left + offsetByPosition(viewport, { x: -1, y: 0 }), + ]; + + partials.forEach((partial: Spacing) => { + expect(isTotallyVisible({ + target: partial, + viewport, + destination: asBigAsViewport, + })).toBe(false); + + // validation + expect(isPartiallyVisible({ + target: partial, + viewport, + destination: asBigAsViewport, + })).toBe(true); + }); + }); + }); + + describe('with changes in droppable scroll', () => { + const clippedByViewport: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'clipped', + type: 'TYPE', + }, + client: getArea({ + top: viewport.top, + // stretches out the bottom of the viewport + bottom: viewport.bottom + 100, + left: viewport.left, + right: viewport.right, + }), + closest: { + frameClient: viewport, + scrollWidth: viewport.width, + scrollHeight: viewport.bottom + 100, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + describe('originally invisible but now invisible', () => { + it('should take into account the droppable scroll when detecting visibility', () => { + const originallyInvisible: Spacing = { + top: viewport.bottom + 1, + bottom: viewport.bottom + 100, + right: viewport.left + 1, + left: viewport.left + 100, + }; + + // originally invisible + expect(isTotallyVisible({ + target: originallyInvisible, + destination: clippedByViewport, + viewport, + })).toBe(false); + + // after scroll the target is now visible + expect(isTotallyVisible({ + target: originallyInvisible, + destination: scrollDroppable(clippedByViewport, { x: 0, y: 100 }), + viewport, + })).toBe(true); + }); + }); + + describe('originally visible but now visible', () => { + it('should take into account the droppable scroll when detecting visibility', () => { + const originallyVisible: Spacing = { + top: viewport.top, + bottom: viewport.top + 50, + right: viewport.left + 1, + left: viewport.left + 100, + }; + + // originally visible + expect(isTotallyVisible({ + target: originallyVisible, + destination: clippedByViewport, + viewport, + })).toBe(true); + + // after scroll the target is now invisible + expect(isTotallyVisible({ + target: originallyVisible, + destination: scrollDroppable(clippedByViewport, { x: 0, y: 100 }), + viewport, + })).toBe(false); + }); + }); + }); + }); + + describe('droppable', () => { + const client: Area = getArea({ + top: 0, + left: 0, + right: 600, + bottom: 600, + }); + const frame: Area = getArea({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }); + + const scrollable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'clipped', + type: 'TYPE', + }, + client, + closest: { + frameClient: frame, + scrollHeight: client.height, + scrollWidth: client.width, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + describe('without changes in droppable scroll', () => { + it('should return false if outside the droppable', () => { + expect(isTotallyVisible({ + target: inViewport2, + viewport, + destination: asBigAsInViewport1, + })).toBe(false); + }); + + it('should return false if the target is bigger than the droppable', () => { + expect(isTotallyVisible({ + target: viewport, + viewport, + destination: asBigAsInViewport1, + })).toBe(false); + }); + + it('should return true if the same size of the droppable', () => { + expect(isTotallyVisible({ + target: inViewport1, + viewport, + destination: asBigAsInViewport1, + })).toBe(true); + }); + + it('should return true if within the droppable', () => { + const insideDroppable: Spacing = { + top: 20, + left: 20, + right: 80, + bottom: 80, + }; + + expect(isTotallyVisible({ + target: insideDroppable, + viewport, + destination: asBigAsInViewport1, + })).toBe(true); + }); + + it('should return false if partially within the droppable', () => { + const partials: Spacing[] = [ + // bleed over top + offsetByPosition(inViewport1, { x: 0, y: -1 }), + // bleed over right + offsetByPosition(inViewport1, { x: 1, y: 0 }), + // bleed over bottom + offsetByPosition(inViewport1, { x: 0, y: 1 }), + // bleed over left + offsetByPosition(inViewport1, { x: -1, y: 0 }), + ]; + + partials.forEach((partial: Spacing) => { + expect(isTotallyVisible({ + target: partial, + viewport, + destination: asBigAsInViewport1, + })).toBe(false); + + // validation + expect(isPartiallyVisible({ + target: partial, + viewport, + destination: asBigAsInViewport1, + })).toBe(true); + }); + }); + + it('should return false if falling on clipped area of droppable', () => { + const ourFrame: Spacing = { + top: 10, + left: 10, + right: 100, + // cuts the droppable short + bottom: 100, + }; + const clippedDroppable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'clipped', + type: 'TYPE', + }, + client: getArea({ + ...ourFrame, + // stretches out past frame + bottom: 600, + }), + closest: { + frameClient: getArea(ourFrame), + scrollHeight: 600, + scrollWidth: getArea(ourFrame).width, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const inSubjectOutsideFrame: Spacing = { + ...frame, + top: 110, + bottom: 200, + }; + + expect(isTotallyVisible({ + target: inSubjectOutsideFrame, + destination: clippedDroppable, + viewport, + })).toBe(false); + }); + }); + + describe('with changes in droppable scroll', () => { + describe('originally invisible but now invisible', () => { + it('should take into account the droppable scroll when detecting visibility', () => { + const originallyInvisible: Spacing = { + left: frame.left, + right: frame.right, + top: frame.bottom + 10, + bottom: frame.bottom + 20, + }; + + // originally invisible + expect(isTotallyVisible({ + target: originallyInvisible, + destination: scrollable, + viewport, + })).toBe(false); + + // after scroll the target is now visible + const scrolled: DroppableDimension = scrollDroppable(scrollable, { x: 0, y: 100 }); + expect(isTotallyVisible({ + target: originallyInvisible, + destination: scrolled, + viewport, + })).toBe(true); + }); + }); + + describe('originally visible but now visible', () => { + it('should take into account the droppable scroll when detecting visibility', () => { + const originallyVisible: Spacing = { + ...frame, + top: 10, + bottom: 20, + }; + + // originally visible + expect(isTotallyVisible({ + target: originallyVisible, + destination: scrollable, + viewport, + })).toBe(true); + + // after scroll the target is now invisible + expect(isTotallyVisible({ + target: originallyVisible, + destination: scrollDroppable(scrollable, { x: 0, y: 100 }), + viewport, + })).toBe(false); + }); + }); + }); + + describe('with invisible subject', () => { + it('should return false when subject is totally invisible', () => { + // creating a droppable where the frame is bigger than the subject + const droppable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'droppable', + type: 'TYPE', + }, + client: getArea({ + top: 0, + left: 0, + bottom: 100, + right: 100, + }), + closest: { + frameClient: getArea({ + top: 0, + left: 0, + bottom: 100, + right: 100, + }), + scrollHeight: 600, + scrollWidth: 600, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + const originallyVisible: Spacing = { + ...frame, + top: 10, + bottom: 20, + }; + + // originally visible + expect(isTotallyVisible({ + target: originallyVisible, + destination: droppable, + viewport, + })).toBe(true); + + // subject is now totally invisible + const scrolled: DroppableDimension = scrollDroppable( + droppable, + getClosestScrollable(droppable).scroll.max, + ); + // asserting frame is not visible + expect(scrolled.viewport.clipped).toBe(null); + + // now asserting that this check will fail + expect(isTotallyVisible({ + target: originallyVisible, + destination: scrolled, + viewport, + })).toBe(false); + }); + }); + }); + + describe('viewport + droppable', () => { + it('should return true if visible in the viewport and the droppable', () => { + expect(isTotallyVisible({ + target: inViewport1, + viewport, + destination: asBigAsInViewport1, + })).toBe(true); + }); + + it('should return false if not visible in the droppable even if visible in the viewport', () => { + expect(isTotallyVisible({ + target: inViewport2, + viewport, + destination: asBigAsInViewport1, + })).toBe(false); + }); + + it('should return false if not visible in the viewport even if visible in the droppable', () => { + const notVisibleDroppable = getDroppableDimension({ + descriptor: { + id: 'not-visible', + type: 'TYPE', + }, + client: getArea(notInViewport), + }); + + expect(isTotallyVisible({ + // is visibile in the droppable + target: notInViewport, + // but not visible in the viewport + viewport, + destination: notVisibleDroppable, + })).toBe(false); + }); + }); +}); diff --git a/test/unit/view/unconnected-droppable.spec.js b/test/unit/view/unconnected-droppable.spec.js index c0e05df8a8..e09cbd5717 100644 --- a/test/unit/view/unconnected-droppable.spec.js +++ b/test/unit/view/unconnected-droppable.spec.js @@ -7,8 +7,17 @@ import Droppable from '../../../src/view/droppable/droppable'; import Placeholder from '../../../src/view/placeholder/'; import { withStore, combine, withDimensionMarshal, withStyleContext } from '../../utils/get-context-options'; import { getPreset } from '../../utils/dimension'; -import type { DroppableId, DraggableDimension } from '../../../src/types'; -import type { MapProps, OwnProps, Provided, StateSnapshot } from '../../../src/view/droppable/droppable-types'; +import type { + DraggableId, + DroppableId, + DraggableDimension, +} from '../../../src/types'; +import type { + MapProps, + OwnProps, + Provided, + StateSnapshot, +} from '../../../src/view/droppable/droppable-types'; const getStubber = (mock: Function) => class Stubber extends Component<{provided: Provided, snapshot: StateSnapshot}> { diff --git a/test/utils/get-simple-state-preset.js b/test/utils/get-simple-state-preset.js index 85b5fb8d1d..938e11136f 100644 --- a/test/utils/get-simple-state-preset.js +++ b/test/utils/get-simple-state-preset.js @@ -266,7 +266,10 @@ export default (axis?: Axis = vertical) => { const allPhases = (id? : DraggableId = preset.inHome1.descriptor.id): State[] => [ idle, preparing, - requesting(id), + requesting({ + draggableId: id, + scrollOptions: scheduled, + }), dragging(id), dropAnimating(id), userCancel(id), From c4ec91c04a5221f65a8de64f30714595317e7938 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 16:19:45 +1100 Subject: [PATCH 149/163] removing comment --- src/state/reducer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state/reducer.js b/src/state/reducer.js index ebee9737b9..69a3574ba2 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -59,7 +59,6 @@ type MoveArgs = {| scrollJumpRequest?: ?Position, |} -// TODO: use map const canPublishDimension = (phase: Phase): boolean => ['IDLE', 'DROP_ANIMATING', 'DROP_COMPLETE'].indexOf(phase) === -1; From 06168c307c4b49c8ebf1b5130224ea6b64cef85b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 20 Feb 2018 20:29:57 +1100 Subject: [PATCH 150/163] updating docs --- README.md | 190 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 70a7ab7fc5..bbef2c2caf 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,16 @@ Avoid the *pull to refresh action* and *delayed anchor focus* on Android Chrome touch-action: manipulation; ``` +#### Always: Droppable + +Styles applied to: **droppable element** using the `data-react-beautiful-dnd-droppable` attribute. + +Opting out of the browser feature which tries to maintain the scroll position when the DOM changes above the fold. We already correctly maintain the scroll position. The automatic `overflow-anchor` behavior leads to incorrect scroll positioning post drop. + +```css +overflow-anchor: none; +``` + ### Phase: resting #### (Phase: resting): drag handle @@ -440,10 +450,17 @@ In order to use drag and drop, you need to have the part of your `React` tree th ```js type Hooks = {| - onDragStart?: (initial: DragStart) => void, - onDragEnd: (result: DropResult) => void, + // optional + onDragStart?: OnDragStartHook, + onDragUpdate?: OnDragUpdateHook, + // always required + onDragEnd: OnDragEndHook, |} +type OnDragStartHook = (start: DragStart, provided: HookProvided) => void; +type OnDragUpdateHook = (update: DragUpdate, provided: HookProvided) => void; +type OnDragEndHook = (result: DropResult, provided: HookProvided) => void; + type Props = {| ...Hooks, children: ?Node, @@ -459,14 +476,18 @@ class App extends React.Component { onDragStart = () => { /*...*/ }; - onDragEnd = () => { + onDragUpdate = () => { /*...*/ + } + onDragEnd = () => { + // the only one that is required }; render() { return (
Hello world
@@ -478,24 +499,48 @@ class App extends React.Component { ### `Hook`s -These are top level application events that you can use to perform your own state updates. +These are top level application events that you can use to perform your own state updates as well as to make screen reader announcements. For more information about controlling the screen reader see our [screen reader guide](TODO) + +### `provided: HookProvided` + +```js +type HookProvided = {| + announce: Announce, +|} + +type Announce = (message: string) => void; +``` + +All hooks are provided with a second argument: `HookProvided`. This object has one property: `announce`. This function is used to synchronously announce a message to screen readers. If you do not use this function we will announce a default english message. We have created a [guide for screen reader usage](TODO) which we recommend using if you are interested in controlling the screen reader messages for yourself and to support internationalisation. If you are using `announce` it must be called synchronously. ### `onDragStart` (optional) -This function will get notified when a drag starts. You are provided with the following details: +```js +type OnDragStartHook = (start: DragStart, provided: HookProvided) => void; +``` + +`onDragStart` will get notified when a drag starts. This hook is *optional* and therefore does not need to be provided. It is **highly recommended** that you use this function to block updates to all `Draggable` and `Droppable` components during a drag. (See [*Best practices for `hooks` *](https://github.com/atlassian/react-beautiful-dnd#best-practices-for-hooks)) -### `initial: DragStart` +You are provided with the following details: -- `initial.draggableId`: the id of the `Draggable` that is now dragging -- `initial.type`: the `type` of the `Draggable` that is now dragging -- `initial.source`: the location (`droppableId` and `index`) of where the dragging item has started within a `Droppable`. +#### `start: DragStart` + +```js +type DragStart = {| + draggableId: DraggableId, + type: TypeId, + source: DraggableLocation, +|} +``` -This function is *optional* and therefore does not need to be provided. It is **highly recommended** that you use this function to block updates to all `Draggable` and `Droppable` components during a drag. (See [*Best practices for `hooks` *](https://github.com/atlassian/react-beautiful-dnd#best-practices-for-hooks)) +- `start.draggableId`: the id of the `Draggable` that is now dragging +- `start.type`: the `type` of the `Draggable` that is now dragging +- `start.source`: the location (`droppableId` and `index`) of where the dragging item has started within a `Droppable`. -### `onDragStart` type information +#### `onDragStart` type information ```js -onDragStart?: (initial: DragStart) => void +type OnDragStartHook = (start: DragStart, provided: HookProvided) => void; // supporting types type DragStart = {| @@ -515,24 +560,63 @@ type DroppableId = Id; type TypeId = Id; ``` +### `onDragUpdate` (optional) + +```js +type OnDragUpdateHook = (update: DragUpdate, provided: HookProvided) => void; +``` + +This function is called whenever something changes during a drag. The possible changes are: + +- The position of the `Draggable` has changed +- The `Draggable` is now over a different `Droppable` +- The `Draggable` is now over no `Droppable` + +It is important that you not do too much work as a result of this function as it will slow down the drag. + +#### `update: DragUpdate` + +```js +type DragUpdate = {| + ...DragStart, + // may not have any destination (drag to nowhere) + destination: ?DraggableLocation, +|} +``` + +- `update.draggableId`: the id of the `Draggable` that is now dragging +- `update.type`: the `type` of the `Draggable` that is now dragging +- `update.source`: the location (`droppableId` and `index`) of where the dragging item has started within a `Droppable`. +- `update.destination`: the location (`droppableId` and `index`) of where the dragging item is now. This can be null if the user is currently not dragging over any `Droppable`. + ### `onDragEnd` (required) This function is *extremely* important and has an critical role to play in the application lifecycle. **This function must result in the *synchronous* reordering of a list of `Draggables`** It is provided with all the information about a drag: -### `result: DropResult` +#### `result: DropResult` + +```js +type DropResult = {| + ...DragUpdate, + reason: DropReason, +|} + +type DropReason = 'DROP' | 'CANCEL'; +``` - `result.draggableId`: the id of the `Draggable` that was dragging. - `result.type`: the `type` of the `Draggable` that was dragging. - `result.source`: the location where the `Draggable` started. -- `result.destination`: the location where the `Draggable` finished. The `destination` will be `null` if the user dropped into no position (such as outside any list) *or* if they dropped the `Draggable` back into the same position in which it started. +- `result.destination`: the location where the `Draggable` finished. The `destination` will be `null` if the user dropped while not over a `Droppable`. +- `result.reason`: the reason a drop occurred. This information can be helpful in crafting more useful messaging in the `HookProvided` > `announce` function. ### Synchronous reordering -Because this library does not control your state, it is up to you to *synchronously* reorder your lists based on the `result`. +Because this library does not control your state, it is up to you to *synchronously* reorder your lists based on the `result: DropResult`. -#### Here is what you need to do: +#### Here is what you need to do - if the `destination` is `null`: all done! - if `source.droppableId` equals `destination.droppableId` you need to remove the item from your list and insert it at the correct position. @@ -542,31 +626,6 @@ Because this library does not control your state, it is up to you to *synchronou If you need to persist a reorder to a remote data store - update the list synchronously on the client and fire off a request in the background to persist the change. If the remote save fails it is up to you how to communicate that to the user and update, or not update, the list. -### `onDragEnd` type information - -```js -onDragEnd: (result: DropResult) => void - -// supporting types -type DropResult = {| - draggableId: DraggableId, - type: TypeId, - source: DraggableLocation, - // may not have any destination (drag to nowhere) - destination: ?DraggableLocation -|} - -type Id = string; -type DroppableId = Id; -type DraggableId = Id; -type TypeId = Id; -type DraggableLocation = {| - droppableId: DroppableId, - // the position of the droppable within a droppable - index: number -|}; -``` - ### Best practices for `hooks` #### Block updates during a drag @@ -583,29 +642,6 @@ Here are a few poor user experiences that can occur if you change things *during - If you remove the node that the user is dragging, then the drag will instantly end - If you change the dimension of the dragging node, then other things will not move out of the way at the correct time. -### Accessibility ❤️ - -All of our lifecycle `hooks` provide the ability to `announce` a change to screen readers. It is a function that accepts a `string` and will print it to the user: - -```js -export type Announce = (message: string) => void; -``` - -Based on the information passed to in the `hooks` you are able to provide meaningful messages to screen readers. - -On lift - -On update -- item has moved index -- item has moved droppable -- item is no longer over a droppable (only possible with pointer based dragging) - -onDragEnd -- item was dropped in a new location -- item was dropped in the same location it started in -- item was dropped while in no location (only possible with pointer based dragging) -- drag was cancelled (may be due a user directly cancelling, a user cancelling indirectly through an action such as a browser resize, or an error). - #### Force focus after a transition between lists When an item is moved from one list to a different list, it loses browser focus if it had it. This is because `React` creates a new node in this situation. It will not lose focus if transitioned within the same list. The dragging item will always have had browser focus if it is dragging with a keyboard. It is highly recommended that you give the item (which is now in a different list) focus again. You can see an example of how to do this in our stories. Here is an example of how you could do it: @@ -703,7 +739,10 @@ export type DroppableProps = {| ```js type DroppableStateSnapshot = {| + // Is the Droppable being dragged over? isDraggingOver: boolean, + // What is the id of the draggable that is dragging over the Droppable? + draggingOverWith: ?DraggableId, |}; ``` @@ -1137,11 +1176,13 @@ const myOnClick = event => console.log('clicked on', event.target);
; ``` -#### 2. snapshot: (DraggableStateSnapshot)** +#### 2. Snapshot: (DraggableStateSnapshot)** ```js type DraggableStateSnapshot = {| isDragging: boolean, + // What Droppable (if any) the Draggable is currently over + draggingOver: ?DroppableId, |}; ``` @@ -1202,14 +1243,25 @@ type DroppableId = Id; type DraggableId = Id; // hooks -type DropResult = {| +type DragStart = {| draggableId: DraggableId, type: TypeId, source: DraggableLocation, +|} + +type DragUpdate = {| + ...DragStart, // may not have any destination (drag to nowhere) - destination: ?DraggableLocation + destination: ?DraggableLocation, |} +type DropResult = {| + ...DragUpdate, + reason: DropReason, +|} + +type DropReason = 'DROP' | 'CANCEL' + type DraggableLocation = {| droppableId: DroppableId, // the position of the droppable within a droppable @@ -1224,6 +1276,7 @@ type DroppableProvided = {| type DroppableStateSnapshot = {| isDraggingOver: boolean, + draggingOverWith: ?DraggableId, |} // Draggable @@ -1236,6 +1289,7 @@ type DraggableProvided = {| type DraggableStateSnapshot = {| isDragging: boolean, + draggingOver: ?DroppableId, |} export type DraggableProps = {| @@ -1268,7 +1322,7 @@ type DragHandleProvided = {| onTouchStart: (event: TouchEvent) => void, onTouchMove: (event: TouchEvent) => void, tabIndex: number, - 'aria-grabbed': boolean, + 'aria-roledescription': string, draggable: boolean, onDragStart: () => boolean, onDrop: () => boolean From d9bb350e0e35b4d600e6cc06759da711324e6159 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 21 Feb 2018 13:30:13 +1100 Subject: [PATCH 151/163] improved logic for opting out of auto scrolling --- README.md | 16 ++---- src/state/auto-scroller/fluid-scroller.js | 10 ++-- .../is-too-big-to-auto-scroll.js | 10 ++-- src/state/auto-scroller/jump-scroller.js | 10 ++-- .../state/auto-scroll/fluid-scroller.spec.js | 53 +++++++++++++++++-- 5 files changed, 74 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index bbef2c2caf..c8a18fff47 100644 --- a/README.md +++ b/README.md @@ -722,7 +722,6 @@ export type DroppableProps = {| |} ``` - ```js {(provided, snapshot) => ( @@ -1341,7 +1340,7 @@ import type { DroppableProvided } from 'react-beautiful-dnd'; If you are using [TypeScript](https://www.typescriptlang.org/) you can use the community maintained [DefinitelyTyped type definitions](https://www.npmjs.com/package/@types/react-beautiful-dnd). [Installation instructions](http://definitelytyped.org/). -### Sample application +### Sample application with flow types We have created a [sample application](https://github.com/alexreardon/react-beautiful-dnd-flow-example) which exercises the flowtypes. It is a super simple `React` project based on [`react-create-app`](https://github.com/facebookincubator/create-react-app). You can use this as a reference to see how to set things up correctly. @@ -1359,19 +1358,14 @@ While code coverage is [not a guarantee of code health](https://stackoverflow.co ### Performance -This codebase is designed to be extremely performant - it is part of its DNA. It builds on prior investigations into `React` performance that you can read about [here](https://medium.com/@alexandereardon/performance-optimisations-for-react-applications-b453c597b191) and [here](https://medium.com/@alexandereardon/performance-optimisations-for-react-applications-round-2-2042e5c9af97). It is designed to perform the minimum number of renders required for each task. - -#### Highlights +This codebase is designed to be **extremely performant** - it is part of its DNA. It is designed to perform the smallest amount of updates possible. You can have a read about performance work done for `react-beautiful-dnd` here: -- using connected-components with memoization to ensure the only components that render are the ones that need to - thanks [`react-redux`](https://github.com/reactjs/react-redux), [`reselect`](https://github.com/reactjs/reselect) and [`memoize-one`](https://github.com/alexreardon/memoize-one) -- all movements are throttled with a [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) - thanks [`raf-schd`](https://github.com/alexreardon/raf-schd) -- memoization is used all over the place - thanks [`memoize-one`](https://github.com/alexreardon/memoize-one) -- conditionally disabling [`pointer-events`](https://developer.mozilla.org/en/docs/Web/CSS/pointer-events) on `Draggable`s while dragging to prevent the browser needing to do redundant work - you can read more about the technique [here](https://www.thecssninja.com/css/pointer-events-60fps) -- Non primary animations are done on the GPU +- [Rethinking drag and drop](https://medium.com/@alexandereardon/rethinking-drag-and-drop-d9f5770b4e6b) +- [Dragging React performance forward](https://medium.com/@alexandereardon/dragging-react-performance-forward-688b30d40a33) ## Size -Great care has been taken to keep the library as light as possible. It is currently **~34kb (gzip)** in size. There could be a smaller net cost if you where already using one of the underlying dependencies. +Great care has been taken to keep the library as light as possible. It is currently **~38kb (gzip)** in size. There could be a smaller net cost if you where already using one of the underlying dependencies. ## Supported browsers diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index 8a0b7a240e..52eee97a54 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -2,7 +2,7 @@ import rafSchd from 'raf-schd'; import getViewport from '../../window/get-viewport'; import { add, apply, isEqual, patch } from '../position'; -import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; +import { isTooBigToAutoScrollDroppable, isTooBigToAutoScrollViewport } from './is-too-big-to-auto-scroll'; import getBestScrollableDroppable from './get-best-scrollable-droppable'; import { horizontal, vertical } from '../axis'; import { @@ -209,7 +209,7 @@ export default ({ const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; const viewport: Area = getViewport(); - if (isTooBigToAutoScroll(viewport, draggable.page.withMargin)) { + if (isTooBigToAutoScrollViewport(viewport, draggable.page.withMargin)) { return; } @@ -236,7 +236,11 @@ export default ({ // We know this has a closestScrollable const closestScrollable: ClosestScrollable = (droppable.viewport.closestScrollable : any); - if (isTooBigToAutoScroll(closestScrollable.frame, draggable.page.withMargin)) { + if (isTooBigToAutoScrollDroppable( + droppable.axis, + closestScrollable.frame, + draggable.page.withMargin + )) { return; } diff --git a/src/state/auto-scroller/is-too-big-to-auto-scroll.js b/src/state/auto-scroller/is-too-big-to-auto-scroll.js index 24c5f6502f..0b35094805 100644 --- a/src/state/auto-scroller/is-too-big-to-auto-scroll.js +++ b/src/state/auto-scroller/is-too-big-to-auto-scroll.js @@ -1,5 +1,9 @@ // @flow -import type { Area } from '../../types'; +import type { Area, Axis } from '../../types'; + +export const isTooBigToAutoScrollDroppable = (axis: Axis, frame: Area, subject: Area): boolean => + subject[axis.size] > frame[axis.size]; + +export const isTooBigToAutoScrollViewport = (viewport: Area, subject: Area): boolean => + subject.width > viewport.width || subject.height > viewport.height; -export default (frame: Area, subject: Area): boolean => - subject.width > frame.width || subject.height > frame.height; diff --git a/src/state/auto-scroller/jump-scroller.js b/src/state/auto-scroller/jump-scroller.js index d318acc0d8..668b4baca1 100644 --- a/src/state/auto-scroller/jump-scroller.js +++ b/src/state/auto-scroller/jump-scroller.js @@ -1,7 +1,7 @@ // @flow import { add, subtract } from '../position'; import getWindowScroll from '../../window/get-window-scroll'; -import isTooBigToAutoScroll from './is-too-big-to-auto-scroll'; +import { isTooBigToAutoScrollDroppable, isTooBigToAutoScrollViewport } from './is-too-big-to-auto-scroll'; import getViewport from '../../window/get-viewport'; import { canScrollDroppable, @@ -75,13 +75,17 @@ export default ({ const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; // 1. Is the draggable too big to auto scroll? - if (isTooBigToAutoScroll(getViewport(), draggable.page.withMargin)) { + if (isTooBigToAutoScrollViewport(getViewport(), draggable.page.withMargin)) { moveByOffset(state, request); return; } if (closestScrollable) { - if (isTooBigToAutoScroll(closestScrollable.frame, draggable.page.withMargin)) { + if (isTooBigToAutoScrollDroppable( + droppable.axis, + closestScrollable.frame, + draggable.page.withMargin + )) { moveByOffset(state, request); return; } diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index 200873bc39..1ffc05c79a 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -642,9 +642,9 @@ describe('fluid auto scrolling', () => { ); }); - it('should not scroll if the item is too big', () => { - const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ + it('should not scroll if the item is too big on the main axis', () => { + const expanded: Area = getArea(expandByPosition(frame, patch(axis.line, 1))); + const tooBigOnMainAxis: DraggableDimension = getDraggableDimension({ descriptor: { id: 'too big', droppableId: preset.home.descriptor.id, @@ -667,12 +667,12 @@ describe('fluid auto scrolling', () => { initial: { // $ExpectError ...base.drag.initial, - descriptor: tooBig.descriptor, + descriptor: tooBigOnMainAxis.descriptor, }, }, }; - return addDroppable(addDraggable(updated, tooBig), scrollable); + return addDroppable(addDraggable(updated, tooBigOnMainAxis), scrollable); })(); autoScroller.onStateChange(state.idle, custom); @@ -681,6 +681,49 @@ describe('fluid auto scrolling', () => { expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); + it('should allow scrolling if the item is bigger than the droppable on the cross axis', () => { + const expanded: Area = getArea(expandByPosition(frame, patch(axis.crossAxisLine, 1))); + const expected: Position = patch(axis.line, config.maxScrollSpeed); + const tooBigOnCrossAxis: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + const selection: Position = onMaxBoundary; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBigOnCrossAxis.descriptor, + }, + }, + }; + + return addDroppable(addDraggable(updated, tooBigOnCrossAxis), scrollable); + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + expected + ); + }); + it('should allow scrolling to the end of the droppable', () => { const target: Position = onEndOfFrame; // scrolling to max scroll point From 42191ad36fe5506253479b1af6245889f2696b58 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 21 Feb 2018 21:02:40 +1100 Subject: [PATCH 152/163] attempt 1 --- .../auto-scroller/adjust-for-size-limits.js | 36 +++++++++++++ src/state/auto-scroller/fluid-scroller.js | 51 ++++++++++++------- .../is-too-big-to-auto-scroll.js | 15 ++++-- src/state/auto-scroller/jump-scroller.js | 2 +- 4 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 src/state/auto-scroller/adjust-for-size-limits.js diff --git a/src/state/auto-scroller/adjust-for-size-limits.js b/src/state/auto-scroller/adjust-for-size-limits.js new file mode 100644 index 0000000000..2c22b9d982 --- /dev/null +++ b/src/state/auto-scroller/adjust-for-size-limits.js @@ -0,0 +1,36 @@ +// @flow +import type { + Position, + Area, +} from '../../types'; + +type Args = {| + container: Area, + subject: Area, + proposedScroll: Position, +|} + +export default ({ + container, + subject, + proposedScroll, +}: Args): ?Position => { + const isTooBigVertically: boolean = subject.height > container.height; + const isTooBigHorizontally: boolean = subject.width > container.width; + + // not too big on any axis + if (!isTooBigHorizontally && !isTooBigVertically) { + return proposedScroll; + } + + // too big on both axis + if (isTooBigHorizontally && isTooBigVertically) { + return null; + } + + // only too big on one axis + return { + x: isTooBigHorizontally ? 0 : proposedScroll.x, + y: isTooBigVertically ? 0 : proposedScroll.y, + }; +}; diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index 52eee97a54..7854a5ff5b 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -9,6 +9,7 @@ import { canScrollWindow, canPartiallyScroll, } from './can-scroll'; +import adjustForSizeLimits from './adjust-for-size-limits'; import type { Area, Axis, @@ -85,8 +86,14 @@ const getSpeed = (distance: number, thresholds: PixelThresholds): number => { return speed; }; +type Args = {| + container: Area, + subject: Area, + center: Position, +|} + // returns null if no scroll is required -const getRequiredScroll = (container: Area, center: Position): ?Position => { +const getRequiredScroll = ({ container, subject, center }: Args): ?Position => { // get distance to each edge const distance: Spacing = { top: center.y - container.top, @@ -130,9 +137,23 @@ const getRequiredScroll = (container: Area, center: Position): ?Position => { const required: Position = clean({ x, y }); - return isEqual(required, origin) ? null : required; + // nothing required + if (isEqual(required, origin)) { + return null; + } + + // need to not scroll in a direction that we are too big to scroll in + return adjustForSizeLimits({ + container, + subject, + proposedScroll: required, + }); }; +// type BlockArgs = {| +// container: Area, +// |} + type WithPlaceholderResult = {| current: Position, max: Position, @@ -207,13 +228,13 @@ export default ({ // 1. Can we scroll the viewport? const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; + const subject: Area = draggable.page.withMargin; const viewport: Area = getViewport(); - - if (isTooBigToAutoScrollViewport(viewport, draggable.page.withMargin)) { - return; - } - - const requiredWindowScroll: ?Position = getRequiredScroll(viewport, center); + const requiredWindowScroll: ?Position = getRequiredScroll({ + container: viewport, + subject, + center, + }); if (requiredWindowScroll && canScrollWindow(requiredWindowScroll)) { scheduleWindowScroll(requiredWindowScroll); @@ -236,15 +257,11 @@ export default ({ // We know this has a closestScrollable const closestScrollable: ClosestScrollable = (droppable.viewport.closestScrollable : any); - if (isTooBigToAutoScrollDroppable( - droppable.axis, - closestScrollable.frame, - draggable.page.withMargin - )) { - return; - } - - const requiredFrameScroll: ?Position = getRequiredScroll(closestScrollable.frame, center); + const requiredFrameScroll: ?Position = getRequiredScroll({ + container: closestScrollable.frame, + subject, + center, + }); if (!requiredFrameScroll) { return; diff --git a/src/state/auto-scroller/is-too-big-to-auto-scroll.js b/src/state/auto-scroller/is-too-big-to-auto-scroll.js index 0b35094805..e469e3a725 100644 --- a/src/state/auto-scroller/is-too-big-to-auto-scroll.js +++ b/src/state/auto-scroller/is-too-big-to-auto-scroll.js @@ -1,9 +1,18 @@ // @flow -import type { Area, Axis } from '../../types'; +import type { Area, Axis, Position } from '../../types'; export const isTooBigToAutoScrollDroppable = (axis: Axis, frame: Area, subject: Area): boolean => subject[axis.size] > frame[axis.size]; -export const isTooBigToAutoScrollViewport = (viewport: Area, subject: Area): boolean => - subject.width > viewport.width || subject.height > viewport.height; +export const isTooBigToAutoScrollViewport = (viewport: Area, subject: Area, proposedScroll: Position): boolean => { + if (proposedScroll.x !== 0 && subject.width > viewport.width) { + return true; + } + + if (proposedScroll.y !== 0 && subject.height > viewport.width) { + return true; + } + + return false; +}; diff --git a/src/state/auto-scroller/jump-scroller.js b/src/state/auto-scroller/jump-scroller.js index 668b4baca1..6a585b1a85 100644 --- a/src/state/auto-scroller/jump-scroller.js +++ b/src/state/auto-scroller/jump-scroller.js @@ -75,7 +75,7 @@ export default ({ const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; // 1. Is the draggable too big to auto scroll? - if (isTooBigToAutoScrollViewport(getViewport(), draggable.page.withMargin)) { + if (isTooBigToAutoScrollViewport(getViewport(), draggable.page.withMargin, request)) { moveByOffset(state, request); return; } From 0b01ac046880e8874b65882bdfadc870d85724f5 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 21 Feb 2018 21:14:02 +1100 Subject: [PATCH 153/163] removing is too big logic from jump scroller (it is not needed) --- src/state/auto-scroller/jump-scroller.js | 25 +----------------------- 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/state/auto-scroller/jump-scroller.js b/src/state/auto-scroller/jump-scroller.js index 6a585b1a85..73c1d177f0 100644 --- a/src/state/auto-scroller/jump-scroller.js +++ b/src/state/auto-scroller/jump-scroller.js @@ -1,8 +1,6 @@ // @flow import { add, subtract } from '../position'; import getWindowScroll from '../../window/get-window-scroll'; -import { isTooBigToAutoScrollDroppable, isTooBigToAutoScrollViewport } from './is-too-big-to-auto-scroll'; -import getViewport from '../../window/get-viewport'; import { canScrollDroppable, canScrollWindow, @@ -17,8 +15,6 @@ import type { Position, State, DraggableLocation, - DraggableDimension, - ClosestScrollable, } from '../../types'; type Args = {| @@ -63,7 +59,6 @@ export default ({ return; } - const draggable: DraggableDimension = state.dimension.draggable[drag.initial.descriptor.id]; const destination: ?DraggableLocation = drag.impact.destination; if (!destination) { @@ -72,30 +67,13 @@ export default ({ } const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; - const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; - - // 1. Is the draggable too big to auto scroll? - if (isTooBigToAutoScrollViewport(getViewport(), draggable.page.withMargin, request)) { - moveByOffset(state, request); - return; - } - - if (closestScrollable) { - if (isTooBigToAutoScrollDroppable( - droppable.axis, - closestScrollable.frame, - draggable.page.withMargin - )) { - moveByOffset(state, request); - return; - } - } // 1. We scroll the droppable first if we can to avoid the draggable // leaving the list if (canScrollDroppable(droppable, request)) { const overlap: ?Position = getDroppableOverlap(droppable, request); + // Droppable can absorb the entire scroll request if (!overlap) { scrollDroppable(droppable.descriptor.id, request); @@ -106,7 +84,6 @@ export default ({ // Let the droppable scroll what it can const whatTheDroppableCanScroll: Position = subtract(request, overlap); - // TODO: is it okay that this is before a move? scrollDroppable(droppable.descriptor.id, whatTheDroppableCanScroll); // Okay, now we need to find out where the rest of the movement can come from. From 6010f90cefa7a5cac398fb3d429cb00503c9299b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 21 Feb 2018 21:40:43 +1100 Subject: [PATCH 154/163] new improved experience for big items --- src/state/auto-scroller/fluid-scroller.js | 13 +- .../is-too-big-to-auto-scroll.js | 18 -- .../state/auto-scroll/fluid-scroller.spec.js | 211 +++++++++--------- 3 files changed, 117 insertions(+), 125 deletions(-) delete mode 100644 src/state/auto-scroller/is-too-big-to-auto-scroll.js diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index 7854a5ff5b..8ce5b8b0e5 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -2,7 +2,6 @@ import rafSchd from 'raf-schd'; import getViewport from '../../window/get-viewport'; import { add, apply, isEqual, patch } from '../position'; -import { isTooBigToAutoScrollDroppable, isTooBigToAutoScrollViewport } from './is-too-big-to-auto-scroll'; import getBestScrollableDroppable from './get-best-scrollable-droppable'; import { horizontal, vertical } from '../axis'; import { @@ -143,16 +142,18 @@ const getRequiredScroll = ({ container, subject, center }: Args): ?Position => { } // need to not scroll in a direction that we are too big to scroll in - return adjustForSizeLimits({ + const limited: ?Position = adjustForSizeLimits({ container, subject, proposedScroll: required, }); -}; -// type BlockArgs = {| -// container: Area, -// |} + if (!limited) { + return null; + } + + return isEqual(limited, origin) ? null : limited; +}; type WithPlaceholderResult = {| current: Position, diff --git a/src/state/auto-scroller/is-too-big-to-auto-scroll.js b/src/state/auto-scroller/is-too-big-to-auto-scroll.js deleted file mode 100644 index e469e3a725..0000000000 --- a/src/state/auto-scroller/is-too-big-to-auto-scroll.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow -import type { Area, Axis, Position } from '../../types'; - -export const isTooBigToAutoScrollDroppable = (axis: Axis, frame: Area, subject: Area): boolean => - subject[axis.size] > frame[axis.size]; - -export const isTooBigToAutoScrollViewport = (viewport: Area, subject: Area, proposedScroll: Position): boolean => { - if (proposedScroll.x !== 0 && subject.width > viewport.width) { - return true; - } - - if (proposedScroll.y !== 0 && subject.height > viewport.width) { - return true; - } - - return false; -}; - diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index 1ffc05c79a..54ccb199ed 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -110,6 +110,10 @@ describe('fluid auto scrolling', () => { describe('window scrolling', () => { const thresholds: PixelThresholds = getPixelThresholds(viewport, axis); + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + viewport, + axis === vertical ? horizontal : vertical, + ); describe('moving forward to end of window', () => { const onStartBoundary: Position = patch( @@ -419,11 +423,6 @@ describe('fluid auto scrolling', () => { // just some light tests to ensure that cross axis moving also works describe('moving forward on the cross axis', () => { - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - viewport, - axis === vertical ? horizontal : vertical, - ); - const onStartBoundary: Position = patch( axis.line, viewport.center[axis.line], @@ -461,11 +460,6 @@ describe('fluid auto scrolling', () => { setWindowScroll(windowScroll); }); - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - viewport, - axis === vertical ? horizontal : vertical, - ); - const onStartBoundary: Position = patch( axis.line, viewport.center[axis.line], @@ -499,6 +493,10 @@ describe('fluid auto scrolling', () => { describe('droppable scrolling', () => { const thresholds: PixelThresholds = getPixelThresholds(frame, axis); + const crossAxisThresholds: PixelThresholds = getPixelThresholds( + frame, + axis === vertical ? horizontal : vertical + ); const maxScrollSpeed: Position = patch(axis.line, config.maxScrollSpeed); beforeEach(() => { @@ -642,88 +640,6 @@ describe('fluid auto scrolling', () => { ); }); - it('should not scroll if the item is too big on the main axis', () => { - const expanded: Area = getArea(expandByPosition(frame, patch(axis.line, 1))); - const tooBigOnMainAxis: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBigOnMainAxis.descriptor, - }, - }, - }; - - return addDroppable(addDraggable(updated, tooBigOnMainAxis), scrollable); - })(); - - autoScroller.onStateChange(state.idle, custom); - - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); - - it('should allow scrolling if the item is bigger than the droppable on the cross axis', () => { - const expanded: Area = getArea(expandByPosition(frame, patch(axis.crossAxisLine, 1))); - const expected: Position = patch(axis.line, config.maxScrollSpeed); - const tooBigOnCrossAxis: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const selection: Position = onMaxBoundary; - const custom: State = (() => { - const base: State = state.dragging( - preset.inHome1.descriptor.id, - selection, - ); - - const updated: State = { - ...base, - drag: { - ...base.drag, - initial: { - // $ExpectError - ...base.drag.initial, - descriptor: tooBigOnCrossAxis.descriptor, - }, - }, - }; - - return addDroppable(addDraggable(updated, tooBigOnCrossAxis), scrollable); - })(); - - autoScroller.onStateChange(state.idle, custom); - - requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - expected - ); - }); - it('should allow scrolling to the end of the droppable', () => { const target: Position = onEndOfFrame; // scrolling to max scroll point @@ -739,6 +655,108 @@ describe('fluid auto scrolling', () => { expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); + describe('big draggable', () => { + const onMaxBoundaryOfBoth: Position = patch( + axis.line, + (frame[axis.size] - thresholds.maxSpeedAt), + (frame[axis.crossAxisSize] - crossAxisThresholds.maxSpeedAt), + ); + + describe('bigger on the main axis', () => { + it('should not allow scrolling on the main axis, but allow scrolling on the cross axis', () => { + const expanded: Area = getArea(expandByPosition(frame, patch(axis.line, 1))); + const tooBigOnMainAxis: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + + const selection: Position = onMaxBoundaryOfBoth; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBigOnMainAxis.descriptor, + }, + }, + }; + + return addDroppable(addDraggable(updated, tooBigOnMainAxis), scrollable); + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + // scroll ocurred on the cross axis, but not on the main axis + patch(axis.crossAxisLine, config.maxScrollSpeed) + ); + }); + }); + + describe('bigger on the cross axis', () => { + it('should not allow scrolling on the cross axis, but allow scrolling on the main axis', () => { + const expanded: Area = getArea( + expandByPosition(frame, patch(axis.crossAxisLine, 1)) + ); + const tooBigOnCrossAxis: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + + const selection: Position = onMaxBoundaryOfBoth; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBigOnCrossAxis.descriptor, + }, + }, + }; + + return addDroppable(addDraggable(updated, tooBigOnCrossAxis), scrollable); + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.flush(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + // scroll ocurred on the main axis, but not on the cross axis + patch(axis.line, config.maxScrollSpeed) + ); + }); + }); + }); + describe('over home list', () => { it('should not scroll if the droppable if moving past the end of the frame', () => { const target: Position = add(onEndOfFrame, patch(axis.line, 1)); @@ -1019,11 +1037,6 @@ describe('fluid auto scrolling', () => { const droppableScroll: Position = patch(axis.crossAxisLine, 10); const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - frame, - axis === vertical ? horizontal : vertical, - ); - const onStartBoundary: Position = patch( axis.line, frame.center[axis.line], @@ -1063,10 +1076,6 @@ describe('fluid auto scrolling', () => { describe('moving backward on the cross axis', () => { const droppableScroll: Position = patch(axis.crossAxisLine, 10); const scrolled: DroppableDimension = scrollDroppable(scrollable, droppableScroll); - const crossAxisThresholds: PixelThresholds = getPixelThresholds( - frame, - axis === vertical ? horizontal : vertical, - ); const onStartBoundary: Position = patch( axis.line, From bd04af7354ac6d2b14a3bb3ae723ea7c735949d9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 21 Feb 2018 21:54:42 +1100 Subject: [PATCH 155/163] more tests --- .../state/auto-scroll/fluid-scroller.spec.js | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index 54ccb199ed..116487135f 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -489,6 +489,150 @@ describe('fluid auto scrolling', () => { expect(request[axis.crossAxisLine]).toBeLessThan(0); }); }); + + describe('big draggable', () => { + const onMaxBoundaryOfBoth: Position = patch( + axis.line, + (viewport[axis.size] - thresholds.maxSpeedAt), + (viewport[axis.crossAxisSize] - crossAxisThresholds.maxSpeedAt), + ); + + describe('bigger on the main axis', () => { + it('should not allow scrolling on the main axis, but allow scrolling on the cross axis', () => { + const expanded: Area = getArea(expandByPosition(viewport, patch(axis.line, 1))); + const tooBigOnMainAxis: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + + const selection: Position = onMaxBoundaryOfBoth; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBigOnMainAxis.descriptor, + }, + }, + }; + + return addDraggable(updated, tooBigOnMainAxis); + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith( + // scroll ocurred on the cross axis, but not on the main axis + patch(axis.crossAxisLine, config.maxScrollSpeed) + ); + }); + }); + + describe('bigger on the cross axis', () => { + it('should not allow scrolling on the cross axis, but allow scrolling on the main axis', () => { + const expanded: Area = getArea( + expandByPosition(viewport, patch(axis.crossAxisLine, 1)) + ); + const tooBigOnCrossAxis: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + + const selection: Position = onMaxBoundaryOfBoth; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBigOnCrossAxis.descriptor, + }, + }, + }; + + return addDraggable(updated, tooBigOnCrossAxis); + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.step(); + expect(mocks.scrollWindow).toHaveBeenCalledWith( + // scroll ocurred on the main axis, but not on the cross axis + patch(axis.line, config.maxScrollSpeed) + ); + }); + }); + + describe('bigger on both axis', () => { + it('should not allow scrolling on any axis', () => { + const expanded: Area = getArea( + expandByPosition(viewport, patch(axis.line, 1, 1)) + ); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + + const selection: Position = onMaxBoundaryOfBoth; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, + }, + }, + }; + + return addDraggable(updated, tooBig); + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.step(); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); + }); + }); }); describe('droppable scrolling', () => { @@ -755,6 +899,50 @@ describe('fluid auto scrolling', () => { ); }); }); + + describe('bigger on both axis', () => { + it('should not allow scrolling on the cross axis, but allow scrolling on the main axis', () => { + const expanded: Area = getArea( + expandByPosition(frame, patch(axis.line, 1, 1)) + ); + const tooBig: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'too big', + droppableId: preset.home.descriptor.id, + // after the last item + index: preset.inHomeList.length, + }, + client: expanded, + }); + + const selection: Position = onMaxBoundaryOfBoth; + const custom: State = (() => { + const base: State = state.dragging( + preset.inHome1.descriptor.id, + selection, + ); + + const updated: State = { + ...base, + drag: { + ...base.drag, + initial: { + // $ExpectError + ...base.drag.initial, + descriptor: tooBig.descriptor, + }, + }, + }; + + return addDroppable(addDraggable(updated, tooBig), scrollable); + })(); + + autoScroller.onStateChange(state.idle, custom); + + requestAnimationFrame.step(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + }); + }); }); describe('over home list', () => { From 8b68af5ec36d3671d139c9c1c93784de458c715b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 21 Feb 2018 21:56:20 +1100 Subject: [PATCH 156/163] removing unneeded jump scroller tests --- .../state/auto-scroll/jump-scroller.spec.js | 81 ------------------- 1 file changed, 81 deletions(-) diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index 48362e2751..faadf69b7d 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -90,43 +90,6 @@ describe('jump auto scrolling', () => { const state = getStatePreset(axis); describe('window scrolling', () => { - it('should not scroll if the item is bigger than the viewport', () => { - const expanded: Area = getArea(expandByPosition(viewport, { x: 1, y: 1 })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const request: Position = patch(axis.line, 1); - const current: State = (() => { - const base: State = state.scrollJumpRequest(request); - - // $ExpectError - not checking for null - base.drag.initial.descriptor = tooBig.descriptor; - - return addDraggable(base, tooBig); - })(); - if (!current.drag) { - throw new Error('invalid state'); - } - const expected: Position = add(current.drag.current.client.selection, request); - - autoScroller.onStateChange(state.idle, current); - - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.move).toHaveBeenCalledWith( - tooBig.descriptor.id, - expected, - origin, - true, - ); - }); - describe('moving forwards', () => { it('should manually move the item if the window is unable to scroll', () => { disableWindowScroll(); @@ -289,50 +252,6 @@ describe('jump auto scrolling', () => { const maxDroppableScroll: Position = scrollable.viewport.closestScrollable.scroll.max; - it('should not scroll if the item is bigger than the droppable', () => { - const expanded: Area = getArea(expandByPosition(frame, { x: 1, y: 1 })); - // setting viewport to be bigger - setViewport(getArea({ - top: 0, - left: 0, - right: 10000, - bottom: 10000, - })); - const tooBig: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'too big', - droppableId: preset.home.descriptor.id, - // after the last item - index: preset.inHomeList.length, - }, - client: expanded, - }); - const request: Position = patch(axis.line, 1); - const current: State = (() => { - const base: State = state.scrollJumpRequest(request); - - // $ExpectError - not checking for null - base.drag.initial.descriptor = tooBig.descriptor; - - return addDroppable(addDraggable(base, tooBig), scrollable); - })(); - if (!current.drag) { - throw new Error('invalid state'); - } - const expected: Position = add(current.drag.current.client.selection, request); - - autoScroller.onStateChange(state.idle, current); - - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.move).toHaveBeenCalledWith( - tooBig.descriptor.id, - expected, - origin, - true, - ); - }); - describe('moving forwards', () => { describe('droppable is able to complete entire scroll', () => { it('should only scroll the droppable', () => { From fb8e3bc61f0a95aafbc0891c57d133f4e12f0f97 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 21 Feb 2018 22:05:38 +1100 Subject: [PATCH 157/163] shuffling code around --- .../auto-scroller/adjust-for-size-limits.js | 36 ------------------ src/state/auto-scroller/fluid-scroller.js | 37 +++++++++++++++++-- .../state/auto-scroll/jump-scroller.spec.js | 27 +------------- 3 files changed, 36 insertions(+), 64 deletions(-) delete mode 100644 src/state/auto-scroller/adjust-for-size-limits.js diff --git a/src/state/auto-scroller/adjust-for-size-limits.js b/src/state/auto-scroller/adjust-for-size-limits.js deleted file mode 100644 index 2c22b9d982..0000000000 --- a/src/state/auto-scroller/adjust-for-size-limits.js +++ /dev/null @@ -1,36 +0,0 @@ -// @flow -import type { - Position, - Area, -} from '../../types'; - -type Args = {| - container: Area, - subject: Area, - proposedScroll: Position, -|} - -export default ({ - container, - subject, - proposedScroll, -}: Args): ?Position => { - const isTooBigVertically: boolean = subject.height > container.height; - const isTooBigHorizontally: boolean = subject.width > container.width; - - // not too big on any axis - if (!isTooBigHorizontally && !isTooBigVertically) { - return proposedScroll; - } - - // too big on both axis - if (isTooBigHorizontally && isTooBigVertically) { - return null; - } - - // only too big on one axis - return { - x: isTooBigHorizontally ? 0 : proposedScroll.x, - y: isTooBigVertically ? 0 : proposedScroll.y, - }; -}; diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index 8ce5b8b0e5..b0b25ed2b5 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -8,7 +8,6 @@ import { canScrollWindow, canPartiallyScroll, } from './can-scroll'; -import adjustForSizeLimits from './adjust-for-size-limits'; import type { Area, Axis, @@ -85,14 +84,46 @@ const getSpeed = (distance: number, thresholds: PixelThresholds): number => { return speed; }; -type Args = {| +type AdjustForSizeLimitsArgs = {| + container: Area, + subject: Area, + proposedScroll: Position, +|} + +const adjustForSizeLimits = ({ + container, + subject, + proposedScroll, +}: AdjustForSizeLimitsArgs): ?Position => { + const isTooBigVertically: boolean = subject.height > container.height; + const isTooBigHorizontally: boolean = subject.width > container.width; + + // not too big on any axis + if (!isTooBigHorizontally && !isTooBigVertically) { + return proposedScroll; + } + + // too big on both axis + if (isTooBigHorizontally && isTooBigVertically) { + return null; + } + + // Only too big on one axis + // Exclude the axis that we cannot scroll on + return { + x: isTooBigHorizontally ? 0 : proposedScroll.x, + y: isTooBigVertically ? 0 : proposedScroll.y, + }; +}; + +type GetRequiredScrollArgs = {| container: Area, subject: Area, center: Position, |} // returns null if no scroll is required -const getRequiredScroll = ({ container, subject, center }: Args): ?Position => { +const getRequiredScroll = ({ container, subject, center }: GetRequiredScrollArgs): ?Position => { // get distance to each edge const distance: Spacing = { top: center.y - container.top, diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index faadf69b7d..6a540e6382 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -4,7 +4,6 @@ import type { Axis, Position, State, - DraggableDimension, DroppableDimension, } from '../../../../src/types'; import type { AutoScroller } from '../../../../src/state/auto-scroller/auto-scroller-types'; @@ -16,9 +15,8 @@ import setWindowScroll, { resetWindowScroll } from '../../../utils/set-window-sc import { vertical, horizontal } from '../../../../src/state/axis'; import createAutoScroller from '../../../../src/state/auto-scroller'; import getStatePreset from '../../../utils/get-simple-state-preset'; -import { getPreset } from '../../../utils/dimension'; -import { expandByPosition } from '../../../../src/state/spacing'; -import { getDraggableDimension, getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; +import { getPreset, addDroppable } from '../../../utils/dimension'; +import { getDroppableDimension, scrollDroppable } from '../../../../src/state/dimension'; import getMaxScroll from '../../../../src/state/get-max-scroll'; const origin: Position = { x: 0, y: 0 }; @@ -34,27 +32,6 @@ const viewport: Area = getArea({ bottom: 1000, }); -const addDroppable = (base: State, droppable: DroppableDimension): State => ({ - ...base, - dimension: { - ...base.dimension, - droppable: { - ...base.dimension.droppable, - [droppable.descriptor.id]: droppable, - }, - }, -}); -const addDraggable = (base: State, draggable: DraggableDimension): State => ({ - ...base, - dimension: { - ...base.dimension, - draggable: { - ...base.dimension.draggable, - [draggable.descriptor.id]: draggable, - }, - }, -}); - const disableWindowScroll = () => { setWindowScrollSize({ scrollHeight: viewport.height, From 6fde4d2c96ef97ce106a257bb00bed572720e25b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 22 Feb 2018 11:30:27 +1100 Subject: [PATCH 158/163] improvements to docs and screen-reader guide --- README.md | 61 +++++++++++++++++++------------ docs/guides/screen-reader.md | 38 +++++++++++++++---- src/state/hooks/message-preset.js | 11 +++++- 3 files changed, 79 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index c8a18fff47..97dff0b45d 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,18 @@ See how beautiful it is for yourself! > We provide different links for touch devices as currently [storybook](https://github.com/storybooks/storybook) does not have a good mobile menu experience [more information](https://github.com/storybooks/storybook/issues/124) +## Basic usage examples + +We have created some basic examples on `codesandbox` for you to play with directly: + +- [simple vertical list](https://codesandbox.io/s/k260nyxq9v) +- [simple horizontal list](https://codesandbox.io/s/mmrp44okvj) + +> Coming soon: a getting starting guide! + ## Upgrading -We have created some upgrade instructions in our release notes to help you upgrade to the latest version! +We have created upgrade instructions in our release notes to help you upgrade to the latest version! - [Upgrading from `4.x` to `5.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v5.0.0); - [Upgrading from `3.x` to `4.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v4.0.0); @@ -35,6 +44,7 @@ We have created some upgrade instructions in our release notes to help you upgra - Plays extremely well with standard browser interactions - Unopinionated styling - No creation of additional wrapper dom nodes - flexbox and focus management friendly! +- Accessible ## Currently supported feature set @@ -42,13 +52,13 @@ We have created some upgrade instructions in our release notes to help you upgra - Horizontal lists ↔ - Movement between lists (▤ ↔ ▤) - Mouse 🐭, keyboard 🎹 and touch 👉📱 (mobile, tablet and so on) support -- Auto scrolling - automatically scroll containers and the window as required during a drag (even with keyboard!!) -- Incredible screen reader support - we provide an greate experience for english screen readers out of the box 📦, while also providing complete customisation control and internationalisation support 💖 +- Auto scrolling - automatically scroll containers and the window as required during a drag (even with keyboard 🔥) +- Incredible screen reader support - we provide an amazing experience for english screen readers out of the box 📦. We also provide complete customisation control and internationalisation support for those who need it 💖 - Conditional [dragging](https://github.com/atlassian/react-beautiful-dnd#props-1) and [dropping](https://github.com/atlassian/react-beautiful-dnd#conditionally-dropping) - Multiple independent lists on the one page -- Flexible item sizes - the draggable items can have different heights (vertical) or widths (horizontal) +- Flexible item sizes - the draggable items can have different heights (vertical lists) or widths (horizontal lists) - Custom drag handles - you can drag a whole item by just a part of it -- A droppable list can be a scroll container (without a scrollable parent) or be the child of a scroll container (that also does not have a scrollable parent) +- A `Droppable` list can be a scroll container (without a scrollable parent) or be the child of a scroll container (that also does not have a scrollable parent) - Independent nested lists - a list can be a child of another list, but you cannot drag items from the parent list into a child list - Server side rendering compatible - Plays well with [nested interactive elements](https://github.com/atlassian/react-beautiful-dnd#interactive-child-elements-within-a-draggable) by default @@ -63,13 +73,6 @@ There are a lot of libraries out there that allow for drag and drop interactions `react-beautiful-dnd` [uses `position: fixed` to position the dragging element](#positioning-ownership). In some layouts, this might break how the element is rendered. One example is a ``-based layout which will lose column widths for dragged ``s. Follow [#103](https://github.com/atlassian/react-beautiful-dnd/issues/103) for updates on support for this use case. -## Basic usage examples on `codesandbox` - -We have created some basic examples for you to play with directly: - -- [vertical list](https://codesandbox.io/s/k260nyxq9v) -- [horizontal list](https://codesandbox.io/s/mmrp44okvj) - ## Driving philosophy: physicality The core design idea of `react-beautiful-dnd` is physicality: we want users to feel like they are moving physical objects around @@ -139,25 +142,37 @@ How it is composed: ### Auto scrolling -When a user drags a `Draggable` near the edge of a scrollable `Droppable` or the `window` we automatically scroll the container as we are able to in order make room for the `Draggable`. +When a user drags a `Draggable` near the edge of a *container* we automatically scroll the container as we are able to in order make room for the `Draggable`. + +> A *container* is either a `Droppable` that is scrollable or has a scroll parent - or the `window`. -#### For mouse and touch inputs +#### For mouse and touch inputs 🐭📱 -When the center of a `Draggable` gets within a small distance from the edge of a container we start auto scrolling. As the user gets closer to the edge of the container we increase the speed of the auto scroll. This acceleration uses an easing function to exponentially increase the rate of acceleration the closer we move towards the edge. We reach a maximum rate of acceleration a small distance from the true edge of a container so that the user does not need to be extremely precise to obtain the maximum scroll speed. +When the center of a `Draggable` gets within a small distance from the edge of a container we start auto scrolling. As the user gets closer to the edge of the container we increase the speed of the auto scroll. This acceleration uses an easing function to exponentially increase the rate of acceleration the closer we move towards the edge. We reach a maximum rate of acceleration a small distance from the true edge of a container so that the user does not need to be extremely precise to obtain the maximum scroll speed. This logic applies for any edge that is scrollable. -#### For keyboard dragging +The distances required for auto scrolling are based on a percentage of the height or width of the container for vertical and horizontal scrolling respectively. By using percentages rather than raw pixel values we are able to have a great experience regardless of the size and shape of your containers. -We also correctly update the scroll position as required when keyboard dragging. In order to move a `Draggable` into the correct position we can do a combination of a `Droppable` scroll, `window` scroll and manual movements to ensure the `Draggable` ends up in the correct position in response to user movement instructions. This is lit 🔥. +##### A note about big `Draggable`s + +If the `Draggable` is bigger than a container on the axis you are trying to scroll - we will not permit scrolling on that axis. For example, if you have a `Draggable` that is longer than the height of the window we will not auto scroll vertically. However, we will still permit scrolling to occur horizontally. + +##### iOS auto scroll shake 📱🤕 + +When auto scrolling on an iOS browser (webkit) the `Draggable` noticeably shakes. This is due to a [bug with webkit](https://bugs.webkit.org/show_bug.cgi?id=181954) that has no known work around. We tried for a long time to work around the issue! If you are interesting in seeing this improved please engage with the [webkit issue](https://bugs.webkit.org/show_bug.cgi?id=181954). + +#### For keyboard dragging 🎹 + +We also correctly update the scroll position as required when keyboard dragging. In order to move a `Draggable` into the correct position we can do a combination of a `Droppable` scroll, `window` scroll and manual movements to ensure the `Draggable` ends up in the correct position in response to user movement instructions. This is boss 🔥. + +This is amazing for users with visual impairments as they can correctly move items around in big lists without needing to use mouse positioning. ### Accessibility Traditionally drag and drop interactions have been exclusively a mouse or touch interaction. This library ships with support for drag and drop interactions **using only a keyboard**. This enables power users to drive their experience entirely from the keyboard. As well as opening up these experiences to users who would have been excluded previously. -In addition to supporting keyboard, we have also audited how the keyboard shortcuts interact with standard browser keyboard interactions. When the user is not dragging they can use their keyboard as they normally would. While dragging we override and disable certain browser shortcuts (such as `tab`) to ensure a fluid experience for the user. - -We also provide **fantastic support for screen readers** to assist users with visual (or other) impairments. We ship with english messaging out of the box. However, you are welcome to override these messages by using the `announce` function that it provided to all of the `DragDropContext > hook` functions. +We provide **fantastic support for screen readers** to assist users with visual (or other) impairments. We ship with english messaging out of the box 📦. However, you are welcome to override these messages by using the `announce` function that it provided to all of the `DragDropContext > hook` functions. -See our [accessibility guide](TODO) for a guide on crafting useful screen reader messaging. +See our [screen reader guide](TODO) for a guide on crafting useful screen reader messaging. ## Mouse dragging @@ -186,7 +201,7 @@ Other than these explicitly blocked keyboard events all standard keyboard events ## Keyboard dragging -`react-beautiful-dnd` supports dragging with only a keyboard +`react-beautiful-dnd` supports dragging with only a keyboard. We have audited how our keyboard shortcuts interact with standard browser keyboard interactions. When the user is not dragging they can use their keyboard as they normally would. While dragging we override and disable certain browser shortcuts (such as `tab`) to ensure a fluid experience for the user. ### Keyboard shortcuts: keyboard dragging @@ -566,7 +581,7 @@ type TypeId = Id; type OnDragUpdateHook = (update: DragUpdate, provided: HookProvided) => void; ``` -This function is called whenever something changes during a drag. The possible changes are: +This hook is called whenever something changes during a drag. The possible changes are: - The position of the `Draggable` has changed - The `Draggable` is now over a different `Droppable` diff --git a/docs/guides/screen-reader.md b/docs/guides/screen-reader.md index 30cbccab96..b9dea6a989 100644 --- a/docs/guides/screen-reader.md +++ b/docs/guides/screen-reader.md @@ -10,11 +10,11 @@ This guide has been written to assist you in creating your own messaging that is For the default messages we have gone for a friendly tone. We have also chosen to use personal language; preferring phases such as 'You have dropped the item' over 'Item dropped'. It is a little more wordy but is a friendlier experience. You are welcome to choose your own tone for your messaging. -## `HookProvided` > `Announce` +## How to control announcements -The `announce` function that is provided to each of the `Hook` functions can be used to provide your own screen reader message. This message will be immediately read out. In order to provide a fast and responsive experience to users **you must provide this message synchronously**. If you attempt to hold onto the `announce` function and call it later it will not work and will just print a warning to the console. Additionally, if you try to call `announce` twice for the same event then only the first will be read by the screen reader with subsequent calls to `announce` being ignored and a warning printed. +The `announce` function that is provided to each of the `DragDropContext` > `Hook` functions can be used to provide your own screen reader message. This message will be immediately read out. In order to provide a fast and responsive experience to users **you must provide this message synchronously**. If you attempt to hold onto the `announce` function and call it later it will not work and will just print a warning to the console. Additionally, if you try to call `announce` twice for the same event then only the first will be read by the screen reader with subsequent calls to `announce` being ignored and a warning printed. -## Step 1: instructions +## Step 1: lift instructions When a user `tabs` to a `Draggable` we need to instruct them on how they can start a drag. We do this by using the `aria-roledescription` property on a `drag handle`. @@ -108,7 +108,7 @@ There are two ways a drop can occur. Either the drag was cancelled or the user r ### Cancel -A `DropResult` object has a `reason` property which can either be `DROP` or `CANCEL`. You can use this to display your cancel annoucement. +A `DropResult` object has a `reason` property which can either be `DROP` or `CANCEL`. You can use this to announce your cancel message. ```js onDragEnd = (result: DropResult, provided: HookProvided) => { @@ -130,10 +130,34 @@ You are also welcome to add information about the size of the list, and the name **Suggestion** "Movement cancelled. The item has returned to list ${listName} to its starting position of ${startPosition} of ${listLength}". +### Drop in the home list -### Drop: in the home list +This announcement should: + +- Inform the user that they have completed the drag +- Let them know what position the item is in now + +**Default message**: "You have dropped the item. It has moved from position ${result.source.index + 1} to ${result.destination.index + 1}" + +You may also want to provide a different message if they drop in the same position that they started in. + +**Default message**: "You have dropped the item. It has been dropped on its starting position of ${result.source.index + 1}" + +### Drop in a foreign list + +The messaging for this scenario should be similar to that of dropping in the home list, with the additional information of what list the item started in and where it finished. + +**Default message**: "You have dropped the item. It has moved from position ${result.source.index + 1} in list ${result.source.droppableId} to position ${result.destination.index + 1} in list ${result.destination.droppableId}" + +You may want to extend this to include the name of the `Droppable` rather than the id. Also, if your `Droppable`s are ordered you may also want to include some positioning information. + +### Drop on no destination + +It is possible for a user to drop on no Droppable. This is not possible to do with a keyboard. However, if a user is using a pointer input such as a mouse. Our messaging is geared towards keyboard usage. However, it is a good idea to provide messaging for this scenario also. -### Drop: in a foreign list +In this message you should: -### Drop: no destination +- Let the user know that they dropped while not over a droppable location +- Let them know where the item has returned to +**Default message**: "The item has been dropped while not over a droppable location. The item has returned to its starting position of ${result.source.index + 1}" diff --git a/src/state/hooks/message-preset.js b/src/state/hooks/message-preset.js index 53d7093b6a..3942dbc74d 100644 --- a/src/state/hooks/message-preset.js +++ b/src/state/hooks/message-preset.js @@ -54,10 +54,19 @@ const onDragEnd = (result: DropResult): string => { // Dropped in home list if (result.source.droppableId === result.destination.droppableId) { + // It is in the position that it started in + if (result.source.index === result.destination.index) { + return ` + You have dropped the item. + It has been dropped on its starting position of ${result.source.index + 1} + `; + } + + // It is in a new position return ` You have dropped the item. It has moved from position ${result.source.index + 1} to ${result.destination.index + 1} - `; + `; } // Dropped in a new list From da2b9ce71d18c7d80afb400c8048e821a53f51d1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 22 Feb 2018 14:16:46 +1100 Subject: [PATCH 159/163] simplified jump-scroll logic --- src/state/auto-scroller/jump-scroller.js | 119 ++++++++++++----------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/src/state/auto-scroller/jump-scroller.js b/src/state/auto-scroller/jump-scroller.js index 73c1d177f0..4d14d83fb9 100644 --- a/src/state/auto-scroller/jump-scroller.js +++ b/src/state/auto-scroller/jump-scroller.js @@ -30,6 +30,8 @@ type Args = {| export type JumpScroller = (state: State) => void; +type Remainder = Position; + export default ({ move, scrollDroppable, @@ -46,6 +48,53 @@ export default ({ move(drag.initial.descriptor.id, client, getWindowScroll(), true); }; + const scrollDroppableAsMuchAsItCan = ( + droppable: DroppableDimension, + change: Position + ): ?Remainder => { + // Droppable cannot absorb any of the scroll + if (!canScrollDroppable(droppable, change)) { + return change; + } + + const overlap: ?Position = getDroppableOverlap(droppable, change); + + // Droppable can absorb the entire change + if (!overlap) { + scrollDroppable(droppable.descriptor.id, change); + return null; + } + + // Droppable can only absorb a part of the change + const whatTheDroppableCanScroll: Position = subtract(change, overlap); + scrollDroppable(droppable.descriptor.id, whatTheDroppableCanScroll); + + const remainder: Position = subtract(change, whatTheDroppableCanScroll); + return remainder; + }; + + const scrollWindowAsMuchAsItCan = (change: Position): ?Remainder => { + // window cannot absorb any of the scroll + if (!canScrollWindow(change)) { + return change; + } + + const overlap: ?Position = getWindowOverlap(change); + + // window can absorb entire scroll + if (!overlap) { + scrollWindow(change); + return null; + } + + // window can only absorb a part of the scroll + const whatTheWindowCanScroll: Position = subtract(change, overlap); + scrollWindow(whatTheWindowCanScroll); + + const remainder: Position = subtract(change, whatTheWindowCanScroll); + return remainder; + }; + const jumpScroller: JumpScroller = (state: State) => { const drag: ?DragState = state.drag; @@ -66,75 +115,29 @@ export default ({ return; } - const droppable: DroppableDimension = state.dimension.droppable[destination.droppableId]; - // 1. We scroll the droppable first if we can to avoid the draggable // leaving the list - if (canScrollDroppable(droppable, request)) { - const overlap: ?Position = getDroppableOverlap(droppable, request); - - // Droppable can absorb the entire scroll request - if (!overlap) { - scrollDroppable(droppable.descriptor.id, request); - return; - } - - // Droppable cannot absorb the entire request - - // Let the droppable scroll what it can - const whatTheDroppableCanScroll: Position = subtract(request, overlap); - scrollDroppable(droppable.descriptor.id, whatTheDroppableCanScroll); - - // Okay, now we need to find out where the rest of the movement can come from. - - const canWindowScrollOverlap: boolean = canScrollWindow(overlap); - - // window cannot absorb overlap: we need to move it - if (!canWindowScrollOverlap) { - moveByOffset(state, overlap); - return; - } - - // how much can the window absorb? - const windowOverlap: ?Position = getWindowOverlap(overlap); + const droppableRemainder: ?Position = scrollDroppableAsMuchAsItCan( + state.dimension.droppable[destination.droppableId], + request, + ); - // window can absorb all of the overlap - if (!windowOverlap) { - scrollWindow(overlap); - return; - } - - // window can only partially absorb overlap - - const whatTheWindowCanScroll: Position = subtract(overlap, windowOverlap); - scrollWindow(whatTheWindowCanScroll); - - // need to move the item by the remainder and scroll the window - moveByOffset(state, windowOverlap); + // droppable absorbed the entire scroll + if (!droppableRemainder) { return; } - // 2. Cannot scroll the droppable - can we scroll the window? + const windowRemainder: ?Position = scrollWindowAsMuchAsItCan(droppableRemainder); - // Cannot scroll the window at all - if (!canScrollWindow(request)) { - moveByOffset(state, request); + // window could absorb all droppable remainder + if (!windowRemainder) { return; } - const overlap: ?Position = getWindowOverlap(request); - - // Window can absorb the entire scroll - if (!overlap) { - scrollWindow(request); - return; - } - - const whatTheWindowCanScroll: Position = subtract(request, overlap); - scrollWindow(whatTheWindowCanScroll); - // manually move to the rest - moveByOffset(state, overlap); + // The entire scroll could not be absorbed by the droppable and window + // so we manually move whatever is left + moveByOffset(state, windowRemainder); }; return jumpScroller; From f5c5ab1dfa26d800a01469627c4d196342f3cf35 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 22 Feb 2018 14:18:04 +1100 Subject: [PATCH 160/163] fixing comment --- src/state/auto-scroller/jump-scroller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/auto-scroller/jump-scroller.js b/src/state/auto-scroller/jump-scroller.js index 4d14d83fb9..33d0054bbd 100644 --- a/src/state/auto-scroller/jump-scroller.js +++ b/src/state/auto-scroller/jump-scroller.js @@ -73,7 +73,7 @@ export default ({ return remainder; }; - const scrollWindowAsMuchAsItCan = (change: Position): ?Remainder => { + const scrollWindowAsMuchAsItCan = (change: Position): ?Position => { // window cannot absorb any of the scroll if (!canScrollWindow(change)) { return change; @@ -130,7 +130,7 @@ export default ({ const windowRemainder: ?Position = scrollWindowAsMuchAsItCan(droppableRemainder); - // window could absorb all droppable remainder + // window could absorb all the droppable remainder if (!windowRemainder) { return; } From e641884d2a20eae12da1391c400ed550abb5c455 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 22 Feb 2018 15:24:28 +1100 Subject: [PATCH 161/163] minor changes in response to PR feedback --- src/state/auto-scroller/fluid-scroller.js | 7 ++++++- src/state/get-max-scroll.js | 2 -- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index b0b25ed2b5..6e1bbe5a84 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -287,7 +287,12 @@ export default ({ } // We know this has a closestScrollable - const closestScrollable: ClosestScrollable = (droppable.viewport.closestScrollable : any); + const closestScrollable: ?ClosestScrollable = droppable.viewport.closestScrollable; + + // this should never happen - just being safe + if (!closestScrollable) { + return; + } const requiredFrameScroll: ?Position = getRequiredScroll({ container: closestScrollable.frame, diff --git a/src/state/get-max-scroll.js b/src/state/get-max-scroll.js index 877afdad80..4036909bb4 100644 --- a/src/state/get-max-scroll.js +++ b/src/state/get-max-scroll.js @@ -27,7 +27,5 @@ export default ({ }; return adjustedMaxScroll; - - // return maxScroll; }; From c8a1146f82d97457ce56731a77d4a35439429029 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 22 Feb 2018 16:57:51 +1100 Subject: [PATCH 162/163] removing comment --- src/view/announcer/announcer.js | 1 - stories/src/accessible/task.jsx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js index 6d413b7a09..9871b2febe 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/announcer/announcer.js @@ -25,7 +25,6 @@ const visuallyHidden: Object = { export default (): Announcer => { const id: string = `react-beautiful-dnd-announcement-${count++}`; - // const id: string = 'react-beautiful-dnd-announcement'; let state: State = { el: null, diff --git a/stories/src/accessible/task.jsx b/stories/src/accessible/task.jsx index e4687df5cc..c358eef323 100644 --- a/stories/src/accessible/task.jsx +++ b/stories/src/accessible/task.jsx @@ -38,6 +38,7 @@ export default class Task extends Component { isDragging={snapshot.isDragging} {...provided.draggableProps} {...provided.dragHandleProps} + aria-roledescription="Draggable task. Press space bar to lift" > {this.props.task.content} From 9763762921f41e444db7b6395b9b0ba492dad58a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 22 Feb 2018 16:58:50 +1100 Subject: [PATCH 163/163] improving comment --- src/view/announcer/announcer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js index 9871b2febe..846238accf 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/announcer/announcer.js @@ -9,7 +9,7 @@ type State = {| let count: number = 0; // https://allyjs.io/tutorials/hiding-elements.html -// Element is v hidden but is readable by screen readers +// Element is visually hidden but is readable by screen readers const visuallyHidden: Object = { position: 'absolute', width: '1px',