From c3cdb8facb5e0ecbc41e9e6824e7bc5556076e54 Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Fri, 25 Aug 2017 11:56:08 +1000 Subject: [PATCH 001/117] add story for dragging between two vertical lists --- stories/5-multiple-vertical-lists-story.js | 26 ++++ stories/src/multiple-vertical/quote-app.js | 142 ++++++++++++++++++++ stories/src/multiple-vertical/quote-list.js | 66 +++++++++ 3 files changed, 234 insertions(+) create mode 100644 stories/5-multiple-vertical-lists-story.js create mode 100644 stories/src/multiple-vertical/quote-app.js create mode 100644 stories/src/multiple-vertical/quote-list.js diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js new file mode 100644 index 0000000000..8d2a5cb3e0 --- /dev/null +++ b/stories/5-multiple-vertical-lists-story.js @@ -0,0 +1,26 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import QuoteApp from './src/multiple-vertical/quote-app'; +import { quotes } from './src/data'; + +const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( + quote => ({ + ...quote, + id: `${namespace}::${quote.id}`, + }) +); + +// I don't want these to be random +const alphaQuotes = quotes.slice(0, 2); +const betaQuotes = quotes.slice(6, 8); + +const initialQuotes = { + alpha: namespaceQuoteIds(alphaQuotes, 'alpha'), + beta: namespaceQuoteIds(betaQuotes, 'beta'), +}; + +storiesOf('multiple vertical lists', module) + .add('simple example', () => ( + + )); diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js new file mode 100644 index 0000000000..f5a928e486 --- /dev/null +++ b/stories/src/multiple-vertical/quote-app.js @@ -0,0 +1,142 @@ +// @flow +import React, { Component } from 'react'; +import styled, { injectGlobal } from 'styled-components'; +import { action } from '@storybook/addon-actions'; +import { DragDropContext } from '../../../src/'; +import QuoteList from './quote-list'; +import { colors, grid } from '../constants'; +import reorder from '../reorder'; +import type { Quote } from '../types'; +import type { DropResult, DragStart } from '../../../src/types'; + +const publishOnDragStart = action('onDragStart'); +const publishOnDragEnd = action('onDragEnd'); + +const Root = styled.div` + background-color: ${colors.blue.deep}; + box-sizing: border-box; + padding: ${grid * 2}px; + min-height: 100vh; + + /* flexbox */ + display: flex; + justify-content: center; + align-items: flex-start; +`; + +const isDraggingClassName = 'is-dragging'; + +type Quotes = { + alpha: Quote[], + beta: Quote[], +} + +type Props = {| + initial: Quotes, + listStyle?: Object, +|} + +type State = {| + quotes: Quotes, +|} + +const resolveDrop = (quotes: Quotes, { source, destination }): Quotes => { + const newQuotes: Quotes = { ...quotes }; + + const movedQuote = quotes[source.droppableId][source.index]; + + Object.entries(newQuotes).forEach(([listId, listQuotes]: [string, Quote[]]) => { + let newListQuotes = [...listQuotes]; + + if (listId === source.droppableId) { + newListQuotes = [ + ...newListQuotes.slice(0, source.index), + ...newListQuotes.slice(source.index + 1), + ]; + } + + if (listId === destination.droppableId) { + newListQuotes = [ + ...newListQuotes.slice(0, destination.index), + movedQuote, + ...newListQuotes.slice(destination.index), + ]; + } + + newQuotes[listId] = newListQuotes; + }); + + return newQuotes; +}; + +export default class QuoteApp extends Component { + /* eslint-disable react/sort-comp */ + props: Props + state: State + + state: State = { + quotes: this.props.initial, + }; + /* eslint-enable react/sort-comp */ + + 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) { + return; + } + + const quotes = resolveDrop(this.state.quotes, result); + + this.setState({ quotes }); + } + + componentDidMount() { + // eslint-disable-next-line no-unused-expressions + injectGlobal` + body.${isDraggingClassName} { + cursor: grabbing; + user-select: none; + } + `; + } + + render() { + const { quotes } = this.state; + const style = { + ...this.props.listStyle, + margin: '0 20px', + }; + + return ( + + + + + + + ); + } +} diff --git a/stories/src/multiple-vertical/quote-list.js b/stories/src/multiple-vertical/quote-list.js new file mode 100644 index 0000000000..d78cceb237 --- /dev/null +++ b/stories/src/multiple-vertical/quote-list.js @@ -0,0 +1,66 @@ +// @flow +import React, { Component } from 'react'; +import styled from 'styled-components'; +import { Droppable, Draggable } from '../../../src'; +import QuoteItem from '../primatives/quote-item'; +import { grid, colors } from '../constants'; +import type { Quote } from '../types'; +import type { + Provided as DroppableProvided, + StateSnapshot as DroppableStateSnapshot, +} from '../../../src/view/droppable/droppable-types'; +import type { + Provided as DraggableProvided, + StateSnapshot as DraggableStateSnapshot, +} from '../../../src/view/draggable/draggable-types'; + +const Container = styled.div` + background-color: ${({ isDraggingOver }) => (isDraggingOver ? colors.blue.lighter : colors.blue.light)}; + display: flex; + flex-direction: column; + padding: ${grid}px; + padding-bottom: 0; + user-select: none; + transition: background-color 0.1s ease; + width: 250px; +`; + +export default class QuoteList extends Component { + props: {| + listId: string, + quotes: Quote[], + listType?: string, + style?: Object, + |} + + render() { + const { listId, listType, style, quotes } = this.props; + return ( + + {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( + + {quotes.map((quote: Quote) => ( + + {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( +
+ + {dragProvided.placeholder} +
+ )} +
+ ))} +
+ )} +
+ ); + } +} From cb38e790f0bf5fe54f7a36c907ff26c0a0227e7c Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Fri, 25 Aug 2017 13:03:58 +1000 Subject: [PATCH 002/117] add story for dragging between two vertical lists (#58) --- stories/5-multiple-vertical-lists-story.js | 26 ++++ stories/src/multiple-vertical/quote-app.js | 142 ++++++++++++++++++++ stories/src/multiple-vertical/quote-list.js | 66 +++++++++ 3 files changed, 234 insertions(+) create mode 100644 stories/5-multiple-vertical-lists-story.js create mode 100644 stories/src/multiple-vertical/quote-app.js create mode 100644 stories/src/multiple-vertical/quote-list.js diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js new file mode 100644 index 0000000000..8d2a5cb3e0 --- /dev/null +++ b/stories/5-multiple-vertical-lists-story.js @@ -0,0 +1,26 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import QuoteApp from './src/multiple-vertical/quote-app'; +import { quotes } from './src/data'; + +const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( + quote => ({ + ...quote, + id: `${namespace}::${quote.id}`, + }) +); + +// I don't want these to be random +const alphaQuotes = quotes.slice(0, 2); +const betaQuotes = quotes.slice(6, 8); + +const initialQuotes = { + alpha: namespaceQuoteIds(alphaQuotes, 'alpha'), + beta: namespaceQuoteIds(betaQuotes, 'beta'), +}; + +storiesOf('multiple vertical lists', module) + .add('simple example', () => ( + + )); diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js new file mode 100644 index 0000000000..f5a928e486 --- /dev/null +++ b/stories/src/multiple-vertical/quote-app.js @@ -0,0 +1,142 @@ +// @flow +import React, { Component } from 'react'; +import styled, { injectGlobal } from 'styled-components'; +import { action } from '@storybook/addon-actions'; +import { DragDropContext } from '../../../src/'; +import QuoteList from './quote-list'; +import { colors, grid } from '../constants'; +import reorder from '../reorder'; +import type { Quote } from '../types'; +import type { DropResult, DragStart } from '../../../src/types'; + +const publishOnDragStart = action('onDragStart'); +const publishOnDragEnd = action('onDragEnd'); + +const Root = styled.div` + background-color: ${colors.blue.deep}; + box-sizing: border-box; + padding: ${grid * 2}px; + min-height: 100vh; + + /* flexbox */ + display: flex; + justify-content: center; + align-items: flex-start; +`; + +const isDraggingClassName = 'is-dragging'; + +type Quotes = { + alpha: Quote[], + beta: Quote[], +} + +type Props = {| + initial: Quotes, + listStyle?: Object, +|} + +type State = {| + quotes: Quotes, +|} + +const resolveDrop = (quotes: Quotes, { source, destination }): Quotes => { + const newQuotes: Quotes = { ...quotes }; + + const movedQuote = quotes[source.droppableId][source.index]; + + Object.entries(newQuotes).forEach(([listId, listQuotes]: [string, Quote[]]) => { + let newListQuotes = [...listQuotes]; + + if (listId === source.droppableId) { + newListQuotes = [ + ...newListQuotes.slice(0, source.index), + ...newListQuotes.slice(source.index + 1), + ]; + } + + if (listId === destination.droppableId) { + newListQuotes = [ + ...newListQuotes.slice(0, destination.index), + movedQuote, + ...newListQuotes.slice(destination.index), + ]; + } + + newQuotes[listId] = newListQuotes; + }); + + return newQuotes; +}; + +export default class QuoteApp extends Component { + /* eslint-disable react/sort-comp */ + props: Props + state: State + + state: State = { + quotes: this.props.initial, + }; + /* eslint-enable react/sort-comp */ + + 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) { + return; + } + + const quotes = resolveDrop(this.state.quotes, result); + + this.setState({ quotes }); + } + + componentDidMount() { + // eslint-disable-next-line no-unused-expressions + injectGlobal` + body.${isDraggingClassName} { + cursor: grabbing; + user-select: none; + } + `; + } + + render() { + const { quotes } = this.state; + const style = { + ...this.props.listStyle, + margin: '0 20px', + }; + + return ( + + + + + + + ); + } +} diff --git a/stories/src/multiple-vertical/quote-list.js b/stories/src/multiple-vertical/quote-list.js new file mode 100644 index 0000000000..d78cceb237 --- /dev/null +++ b/stories/src/multiple-vertical/quote-list.js @@ -0,0 +1,66 @@ +// @flow +import React, { Component } from 'react'; +import styled from 'styled-components'; +import { Droppable, Draggable } from '../../../src'; +import QuoteItem from '../primatives/quote-item'; +import { grid, colors } from '../constants'; +import type { Quote } from '../types'; +import type { + Provided as DroppableProvided, + StateSnapshot as DroppableStateSnapshot, +} from '../../../src/view/droppable/droppable-types'; +import type { + Provided as DraggableProvided, + StateSnapshot as DraggableStateSnapshot, +} from '../../../src/view/draggable/draggable-types'; + +const Container = styled.div` + background-color: ${({ isDraggingOver }) => (isDraggingOver ? colors.blue.lighter : colors.blue.light)}; + display: flex; + flex-direction: column; + padding: ${grid}px; + padding-bottom: 0; + user-select: none; + transition: background-color 0.1s ease; + width: 250px; +`; + +export default class QuoteList extends Component { + props: {| + listId: string, + quotes: Quote[], + listType?: string, + style?: Object, + |} + + render() { + const { listId, listType, style, quotes } = this.props; + return ( + + {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( + + {quotes.map((quote: Quote) => ( + + {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( +
+ + {dragProvided.placeholder} +
+ )} +
+ ))} +
+ )} +
+ ); + } +} From ce9f9970cf44339752626f005c29df08e873d50b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 25 Aug 2017 16:19:52 +1000 Subject: [PATCH 003/117] work in progress (#59) --- .eslintrc | 9 +- src/state/axis.js | 6 ++ src/state/get-best-droppable-rules.md | 43 +++++++++ src/state/get-best-droppable.js | 131 ++++++++++++++++++++++++++ src/state/get-best-location.js | 5 + src/state/position.js | 7 ++ src/types.js | 6 ++ test/unit/state/position.spec.js | 41 +++++++- 8 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 src/state/get-best-droppable-rules.md create mode 100644 src/state/get-best-droppable.js create mode 100644 src/state/get-best-location.js diff --git a/.eslintrc b/.eslintrc index d573cc4fe9..57c598119f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -87,6 +87,13 @@ // All blocks must be wrapped in curly braces {} // Preventing if(condition) return; // https://eslint.org/docs/rules/curly - "curly": ["error", "all"] + "curly": ["error", "all"], + + // Allowing Math.pow rather than forcing `**` + // https://eslint.org/docs/rules/no-restricted-properties + "no-restricted-properties": ["off", { + "object": "Math", + "property": "pow" + }] } } \ No newline at end of file diff --git a/src/state/axis.js b/src/state/axis.js index aa1a553fd7..1ff8c48c2b 100644 --- a/src/state/axis.js +++ b/src/state/axis.js @@ -7,6 +7,9 @@ export const vertical: VerticalAxis = { start: 'top', end: 'bottom', size: 'height', + crossAxisStart: 'left', + crossAxisEnd: 'right', + crossAxisSize: 'width', }; export const horizontal: HorizontalAxis = { @@ -15,4 +18,7 @@ export const horizontal: HorizontalAxis = { start: 'left', end: 'right', size: 'width', + crossAxisStart: 'top', + crossAxisEnd: 'bottom', + crossAxisSize: 'height', }; diff --git a/src/state/get-best-droppable-rules.md b/src/state/get-best-droppable-rules.md new file mode 100644 index 0000000000..0ec7b58094 --- /dev/null +++ b/src/state/get-best-droppable-rules.md @@ -0,0 +1,43 @@ + +## Rules for finding the best Droppable: + +### 1. Find lists on the cross axis + +Find the list(s) that are closest on the cross axis + +Conditions +1. The list must have one corner with the size (height: vertical) of the source list +2. The list must be visible to the user + +If more than one list is as close on the cross axis, then: + +### 2. Find the closest corner + +Based on the draggable items current center position, we need to find the list that +has the closest corner point. That is the closest list. + +We do not need to consider the conditions in step 1 as they have already been applied + +## Rules for finding the best location within a Droppable + +### If moving on the main axis +Move into the first / last position depending on if you are leaving the front / back of the Droppable + +nice + +### If moving on the cross axis + +#### Moving to empty list + +Move to the top (vertical list) / left (horizontal list) of the list + +#### Moving to populated list + +1. Find the draggable with the closest center position + +If there is more than one with the closest - choose the the one closest to the top left corner of the page + +2. Move below the item if the Draggables current center position is less than the destination. Otherwise, move above +Below = go below +Above = go above +Equal = go above \ No newline at end of file diff --git a/src/state/get-best-droppable.js b/src/state/get-best-droppable.js new file mode 100644 index 0000000000..b78c419356 --- /dev/null +++ b/src/state/get-best-droppable.js @@ -0,0 +1,131 @@ +// @flow +import memoizeOne from 'memoize-one'; +import { distance } from './position'; +import type { + Axis, + Position, + DimensionFragment, + DraggableId, + DroppableId, + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, + DroppableDimensionMap, +} from '../types'; + +type DroppableCornerMap = {| + [id: DroppableId]: Position[], +|} + +type GetBestDroppableArgs = {| + draggableId: DraggableId, + center: Position, + isMovingForward: boolean, + plane: 'main-axis' | 'cross-axis', + // the droppable the draggable is currently in + droppableId: DroppableId, + droppables: DroppableDimensionMap, + draggables: DraggableDimensionMap, +|} + +const sortOnCrossAxis = memoizeOne( + (droppables: DroppableDimensionMap, axis: Axis): DroppableDimension[] => + Object.keys(droppables) + .map((key: DroppableId): DroppableDimension => droppables[key]) + .sort((a: DroppableDimension, b: DroppableDimension) => ( + a.page.withMargin[axis.crossAxisStart] - b.page.withMargin[axis.crossAxisStart] + ) + ) +); + +type IsWithResultFn = (number) => boolean; + +const isWithin = (lowerBound: number, upperBound: number): IsWithResultFn => + (value: number): boolean => value <= upperBound && value >= lowerBound; + +export default ({ + isMovingForward, + draggableId, + center, + droppableId, + droppables, + draggables, +}: GetBestDroppableArgs): ?DroppableId => { + const draggable: DraggableDimension = draggables[draggableId]; + const source: DroppableDimension = droppables[droppableId]; + const axis: Axis = source.axis; + + const sorted: DroppableDimension[] = sortOnCrossAxis(droppables, axis); + + const candidates: DroppableDimension[] = + // 1. Remove the source droppable from the list + sorted.filter((droppable: DroppableDimension): boolean => droppable !== source) + // 2. Get only droppables that are on the desired side + .filter((droppable: DroppableDimension): boolean => { + if (isMovingForward) { + // is the droppable in front of the source on the cross axis? + return source.page.withMargin[axis.crossAxisEnd] <= + droppable.page.withMargin[axis.crossAxisStart]; + } + // is the droppable behind the source on the cross axis? + return droppable.page.withMargin[axis.crossAxisEnd] <= + source.page.withMargin[axis.crossAxisStart]; + }) + // 3. is there any overlap on the main axis? + .filter((droppable: DroppableDimension): boolean => { + const sourceFragment: DimensionFragment = source.page.withMargin; + const destinationFragment: DimensionFragment = droppable.page.withMargin; + + const isBetweenSourceBounds = isWithin( + sourceFragment[axis.start], + sourceFragment[axis.end] + ); + const isBetweenDestBounds = isWithin( + destinationFragment[axis.start], + destinationFragment[axis.end] + ); + + return isBetweenSourceBounds(destinationFragment[axis.start]) || + isBetweenSourceBounds(destinationFragment[axis.end]) || + isBetweenDestBounds(sourceFragment[axis.start]) || + isBetweenDestBounds(sourceFragment[axis.end]); + }) + // 4. Find the droppables that have the same cross axis value as the first item + .filter((droppable: DroppableDimension, index: number, array: DroppableDimension[]): boolean => + droppable.page.withMargin[axis.crossAxisStart] === + array[0].page.withMargin[axis.crossAxisStart] + ); + + // no possible candidates + if (!candidates.length) { + return null; + } + + // only one result - all done! + if (candidates.length === 1) { + return candidates[0].id; + } + + // At this point we have a number of candidates that + // all have the same axis.crossAxisStart value. + // Now need to consider the main axis as a tiebreaker + + // 1. Get the distance to all of the corner points + // 2. Find the closest corner to current center + // 3. in the event of a tie: choose the corner that is closest to {x: 0, y: 0} + const items: DroppableDimension[] = + candidates.map((droppable: DroppableDimension): DroppableCornerMap => { + const fragment: DimensionFragment = droppable.page.withMargin; + const first: Position = { + x: fragment[axis.crossAxisStart], + y: fragment[axis.start], + }; + const second: Position = { + x: 2, + y: 3, + }; + return { + [droppable.id]: [first, second], + }; + }); +}; diff --git a/src/state/get-best-location.js b/src/state/get-best-location.js new file mode 100644 index 0000000000..6ff684a4ae --- /dev/null +++ b/src/state/get-best-location.js @@ -0,0 +1,5 @@ +// @flow + +// finds best location for a draggable moving between droppables + +// will return the offset for the draggable to move, and the impact of the drag diff --git a/src/state/position.js b/src/state/position.js index 045d4307f9..f792d736bd 100644 --- a/src/state/position.js +++ b/src/state/position.js @@ -25,3 +25,10 @@ export const patch = (line: 'x' | 'y', value: number): Position => ({ y: line === 'y' ? value : 0, }); +// Returns the distance between two points +// https://www.mathsisfun.com/algebra/distance-2-points.html +export const distance = (point1: Position, point2: Position): number => + Math.sqrt( + Math.pow((point2.x - point1.x), 2) + + Math.pow((point2.y - point1.y), 2) + ); diff --git a/src/types.js b/src/types.js index 0dcba025d0..0b12d8aa41 100644 --- a/src/types.js +++ b/src/types.js @@ -21,6 +21,9 @@ export type VerticalAxis = {| start: 'top', end: 'bottom', size: 'height', + crossAxisStart: 'left', + crossAxisEnd: 'right', + crossAxisSize: 'width', |} export type HorizontalAxis = {| @@ -29,6 +32,9 @@ export type HorizontalAxis = {| start: 'left', end: 'right', size: 'width', + crossAxisStart: 'top', + crossAxisEnd: 'bottom', + crossAxisSize: 'height', |} export type Axis = VerticalAxis | HorizontalAxis diff --git a/test/unit/state/position.spec.js b/test/unit/state/position.spec.js index dde74ee3d7..6ba24b25c3 100644 --- a/test/unit/state/position.spec.js +++ b/test/unit/state/position.spec.js @@ -1,5 +1,12 @@ // @flow -import { add, subtract, isEqual, negate, patch } from '../../../src/state/position'; +import { + add, + subtract, + isEqual, + negate, + patch, + distance, +} from '../../../src/state/position'; import type { Position } from '../../../src/types'; const point1: Position = { @@ -10,6 +17,7 @@ const point2: Position = { x: 2, y: 1, }; +const origin: Position = { x: 0, y: 0 }; describe('position', () => { describe('add', () => { @@ -64,4 +72,35 @@ describe('position', () => { expect(patch('y', 5)).toEqual({ x: 0, y: 5 }); }); }); + + describe('distance', () => { + describe('on the same axis', () => { + it('should return the distance between two positive values', () => { + const a = { x: 0, y: 2 }; + const b = { x: 0, y: 5 }; + expect(distance(a, b)).toEqual(3); + }); + + it('should return the distance between two negative values', () => { + const a = { x: 0, y: -2 }; + const b = { x: 0, y: -5 }; + expect(distance(a, b)).toEqual(3); + }); + + it('should return the distance between a positive and negative value', () => { + const a = { x: 0, y: -2 }; + const b = { x: 0, y: 3 }; + expect(distance(a, b)).toEqual(5); + }); + }); + + describe('with axis shift', () => { + it('should account for a shift in plane', () => { + // a '3, 4, 5' triangle + // https://www.mathsisfun.com/pythagoras.html + const target = { x: 3, y: 4 }; + expect(distance(origin, target)).toEqual(5); + }); + }); + }); }); From 1f9640cb2add6f95c1c1d7df955d15e3e926d8f2 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 25 Aug 2017 17:17:16 +1000 Subject: [PATCH 004/117] more progress (#60) --- ...le.js => get-best-cross-axis-droppable.js} | 81 +++++++++---------- src/state/get-best-droppable-rules.md | 43 ---------- src/state/get-best-main-axis-droppable.js | 60 ++++++++++++++ src/state/position.js | 4 + 4 files changed, 103 insertions(+), 85 deletions(-) rename src/state/{get-best-droppable.js => get-best-cross-axis-droppable.js} (71%) delete mode 100644 src/state/get-best-droppable-rules.md create mode 100644 src/state/get-best-main-axis-droppable.js diff --git a/src/state/get-best-droppable.js b/src/state/get-best-cross-axis-droppable.js similarity index 71% rename from src/state/get-best-droppable.js rename to src/state/get-best-cross-axis-droppable.js index b78c419356..e6546b731a 100644 --- a/src/state/get-best-droppable.js +++ b/src/state/get-best-cross-axis-droppable.js @@ -1,31 +1,18 @@ // @flow import memoizeOne from 'memoize-one'; -import { distance } from './position'; +import { closest } from './position'; import type { Axis, Position, DimensionFragment, - DraggableId, DroppableId, - DraggableDimension, DroppableDimension, - DraggableDimensionMap, DroppableDimensionMap, } from '../types'; -type DroppableCornerMap = {| - [id: DroppableId]: Position[], -|} - -type GetBestDroppableArgs = {| - draggableId: DraggableId, - center: Position, - isMovingForward: boolean, - plane: 'main-axis' | 'cross-axis', - // the droppable the draggable is currently in - droppableId: DroppableId, - droppables: DroppableDimensionMap, - draggables: DraggableDimensionMap, +type DistanceToDroppable = {| + id: DroppableId, + distance: number, |} const sortOnCrossAxis = memoizeOne( @@ -38,23 +25,35 @@ const sortOnCrossAxis = memoizeOne( ) ); -type IsWithResultFn = (number) => boolean; - -const isWithin = (lowerBound: number, upperBound: number): IsWithResultFn => +const isWithin = (lowerBound: number, upperBound: number): ((number) => boolean) => (value: number): boolean => value <= upperBound && value >= lowerBound; +const getCorners = (droppable: DroppableDimension): Position[] => { + const fragment: DimensionFragment = droppable.page.withMargin; + + return [ + { x: fragment.left, y: fragment.top }, + { x: fragment.right, y: fragment.top }, + { x: fragment.left, y: fragment.bottom }, + { x: fragment.right, y: fragment.bottom }, + ]; +}; + +type GetBestDroppableArgs = {| + isMovingForward: boolean, + center: Position, + droppableId: DroppableId, + droppables: DroppableDimensionMap, +|} + export default ({ isMovingForward, - draggableId, center, droppableId, droppables, - draggables, }: GetBestDroppableArgs): ?DroppableId => { - const draggable: DraggableDimension = draggables[draggableId]; const source: DroppableDimension = droppables[droppableId]; const axis: Axis = source.axis; - const sorted: DroppableDimension[] = sortOnCrossAxis(droppables, axis); const candidates: DroppableDimension[] = @@ -80,15 +79,15 @@ export default ({ sourceFragment[axis.start], sourceFragment[axis.end] ); - const isBetweenDestBounds = isWithin( + const isBetweenDestinationBounds = isWithin( destinationFragment[axis.start], destinationFragment[axis.end] ); return isBetweenSourceBounds(destinationFragment[axis.start]) || isBetweenSourceBounds(destinationFragment[axis.end]) || - isBetweenDestBounds(sourceFragment[axis.start]) || - isBetweenDestBounds(sourceFragment[axis.end]); + isBetweenDestinationBounds(sourceFragment[axis.start]) || + isBetweenDestinationBounds(sourceFragment[axis.end]); }) // 4. Find the droppables that have the same cross axis value as the first item .filter((droppable: DroppableDimension, index: number, array: DroppableDimension[]): boolean => @@ -113,19 +112,17 @@ export default ({ // 1. Get the distance to all of the corner points // 2. Find the closest corner to current center // 3. in the event of a tie: choose the corner that is closest to {x: 0, y: 0} - const items: DroppableDimension[] = - candidates.map((droppable: DroppableDimension): DroppableCornerMap => { - const fragment: DimensionFragment = droppable.page.withMargin; - const first: Position = { - x: fragment[axis.crossAxisStart], - y: fragment[axis.start], - }; - const second: Position = { - x: 2, - y: 3, - }; - return { - [droppable.id]: [first, second], - }; - }); + const bestId: DroppableId = + candidates.map((droppable: DroppableDimension): DistanceToDroppable => ({ + id: droppableId, + // two of the corners will be redundant, but it is *way* easier + // to pass every corner than to conditionally grab the right ones + distance: closest(center, getCorners(droppable)), + })) + // the item with the shortest distance will be first + .sort((a: DistanceToDroppable, b: DistanceToDroppable) => a.distance - b.distance) + // TODO: what if there is a tie? + .map(a => a.id)[0]; + + return bestId; }; diff --git a/src/state/get-best-droppable-rules.md b/src/state/get-best-droppable-rules.md deleted file mode 100644 index 0ec7b58094..0000000000 --- a/src/state/get-best-droppable-rules.md +++ /dev/null @@ -1,43 +0,0 @@ - -## Rules for finding the best Droppable: - -### 1. Find lists on the cross axis - -Find the list(s) that are closest on the cross axis - -Conditions -1. The list must have one corner with the size (height: vertical) of the source list -2. The list must be visible to the user - -If more than one list is as close on the cross axis, then: - -### 2. Find the closest corner - -Based on the draggable items current center position, we need to find the list that -has the closest corner point. That is the closest list. - -We do not need to consider the conditions in step 1 as they have already been applied - -## Rules for finding the best location within a Droppable - -### If moving on the main axis -Move into the first / last position depending on if you are leaving the front / back of the Droppable - -nice - -### If moving on the cross axis - -#### Moving to empty list - -Move to the top (vertical list) / left (horizontal list) of the list - -#### Moving to populated list - -1. Find the draggable with the closest center position - -If there is more than one with the closest - choose the the one closest to the top left corner of the page - -2. Move below the item if the Draggables current center position is less than the destination. Otherwise, move above -Below = go below -Above = go above -Equal = go above \ No newline at end of file diff --git a/src/state/get-best-main-axis-droppable.js b/src/state/get-best-main-axis-droppable.js new file mode 100644 index 0000000000..3fa895ad4e --- /dev/null +++ b/src/state/get-best-main-axis-droppable.js @@ -0,0 +1,60 @@ +// @flow +import memoizeOne from 'memoize-one'; +import { closest } from './position'; +import type { + Axis, + Position, + DimensionFragment, + DroppableId, + DroppableDimension, + DroppableDimensionMap, +} from '../types'; + +type DistanceToDroppable = {| + id: DroppableId, + distance: number, +|} + +const sortOnMainAxis = memoizeOne( + (droppables: DroppableDimensionMap, axis: Axis): DroppableDimension[] => + Object.keys(droppables) + .map((key: DroppableId): DroppableDimension => droppables[key]) + .sort((a: DroppableDimension, b: DroppableDimension) => ( + a.page.withMargin[axis.start] - b.page.withMargin[axis.start] + ) + ) +); + +const isWithin = (lowerBound: number, upperBound: number): ((number) => boolean) => + (value: number): boolean => value <= upperBound && value >= lowerBound; + +const getCorners = (droppable: DroppableDimension): Position[] => { + const fragment: DimensionFragment = droppable.page.withMargin; + + return [ + { x: fragment.left, y: fragment.top }, + { x: fragment.right, y: fragment.top }, + { x: fragment.left, y: fragment.bottom }, + { x: fragment.right, y: fragment.bottom }, + ]; +}; + +type GetBestDroppableArgs = {| + isMovingForward: boolean, + center: Position, + droppableId: DroppableId, + droppables: DroppableDimensionMap, +|} + +export default ({ + isMovingForward, + center, + droppableId, + droppables, +}: GetBestDroppableArgs): ?DroppableId => { + const source: DroppableDimension = droppables[droppableId]; + const axis: Axis = source.axis; + const sorted: DroppableDimension[] = sortOnMainAxis(droppables, axis); + + // ... +}; diff --git a/src/state/position.js b/src/state/position.js index f792d736bd..35db5637af 100644 --- a/src/state/position.js +++ b/src/state/position.js @@ -32,3 +32,7 @@ export const distance = (point1: Position, point2: Position): number => Math.pow((point2.x - point1.x), 2) + Math.pow((point2.y - point1.y), 2) ); + +// 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))); From ceee56999a2fc98b8130bd776be6f03cff062f69 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Sun, 27 Aug 2017 21:39:55 +1000 Subject: [PATCH 005/117] wip --- src/state/action-creators.js | 22 ++++++ src/state/dimension-map-to-list.js | 19 +++++ src/state/get-best-location.js | 5 -- src/state/get-draggables-inside-droppable.js | 30 ++++--- src/state/get-droppable-over.js | 28 +++++-- src/state/is-inside-droppable.js | 16 ---- .../get-best-cross-axis-droppable.js | 33 ++++---- .../get-closest-draggable.js | 27 +++++++ src/state/move-to-droppable/index.js | 79 +++++++++++++++++++ .../move-to-droppable/move-to-new-spot.js | 14 ++++ src/state/reducer.js | 23 ++++++ 11 files changed, 236 insertions(+), 60 deletions(-) create mode 100644 src/state/dimension-map-to-list.js delete mode 100644 src/state/get-best-location.js delete mode 100644 src/state/is-inside-droppable.js rename src/state/{ => move-to-droppable}/get-best-cross-axis-droppable.js (82%) create mode 100644 src/state/move-to-droppable/get-closest-draggable.js create mode 100644 src/state/move-to-droppable/index.js create mode 100644 src/state/move-to-droppable/move-to-new-spot.js diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 8fd589ccc0..84600008ec 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -190,6 +190,26 @@ export const moveForward = (id: DraggableId): MoveForwardAction => ({ payload: id, }); +export type CrossAxisMoveForwardAction = {| + type: 'CROSS_AXIS_MOVE_FORWARD', + payload: DraggableId +|} + +export const crossAxisMoveForward = (id: DraggableId): MoveForwardAction => ({ + type: 'CROSS_AXIS_MOVE_FORWARD', + payload: id, +}); + +export type CrossAxisMoveBackwardAction = {| + type: 'CROSS_AXIS_MOVE_BACKWARD', + payload: DraggableId +|} + +export const crossAxisMoveBackward = (id: DraggableId): MoveForwardAction => ({ + type: 'CROSS_AXIS_MOVE_BACKWARD', + payload: id, +}); + type CleanAction = { type: 'CLEAN', payload: null, @@ -449,6 +469,8 @@ export type Action = BeginLiftAction | MoveAction | MoveBackwardAction | MoveForwardAction | + CrossAxisMoveForwardAction | + CrossAxisMoveBackwardAction | DropAnimateAction | DropCompleteAction | CleanAction; diff --git a/src/state/dimension-map-to-list.js b/src/state/dimension-map-to-list.js new file mode 100644 index 0000000000..86e13027a6 --- /dev/null +++ b/src/state/dimension-map-to-list.js @@ -0,0 +1,19 @@ +// @flow +import memoizeOne from 'memoize-one'; +import type { + DraggableId, + DroppableId, + DraggableDimensionMap, + DroppableDimensionMap, +} from '../types'; + +export const droppableMapToList = memoizeOne( + (droppables: DroppableDimensionMap) => + Object.keys(droppables).map((id: DroppableId) => droppables[id]) +); + +export const draggableMapToList = memoizeOne( + (draggables: DraggableDimensionMap) => + Object.keys(draggables).map((id: DraggableId) => draggables[id]) +) +; diff --git a/src/state/get-best-location.js b/src/state/get-best-location.js deleted file mode 100644 index 6ff684a4ae..0000000000 --- a/src/state/get-best-location.js +++ /dev/null @@ -1,5 +0,0 @@ -// @flow - -// finds best location for a draggable moving between droppables - -// will return the offset for the draggable to move, and the impact of the drag diff --git a/src/state/get-draggables-inside-droppable.js b/src/state/get-draggables-inside-droppable.js index eced228a18..857ce8f3b6 100644 --- a/src/state/get-draggables-inside-droppable.js +++ b/src/state/get-draggables-inside-droppable.js @@ -1,25 +1,23 @@ // @flow import memoizeOne from 'memoize-one'; +import { draggableMapToList } from './dimension-map-to-list'; import type { DraggableDimension, DroppableDimension, DraggableDimensionMap, - DraggableId, } from '../types'; export default memoizeOne( - (droppableDimension: DroppableDimension, - draggableDimensions: DraggableDimensionMap, - ): DraggableDimension[] => - Object.keys(draggableDimensions) - .map((key: DraggableId): DraggableDimension => draggableDimensions[key]) - .filter((dimension: DraggableDimension): boolean => - dimension.droppableId === droppableDimension.id - ) - // Dimensions are not guarenteed to be ordered in the same order as keys - // So we need to sort them so they are in the correct order - .sort((a: DraggableDimension, b: DraggableDimension): number => - a.page.withoutMargin.center[droppableDimension.axis.line] - - b.page.withoutMargin.center[droppableDimension.axis.line] - ) - ); + (droppable: DroppableDimension, + draggables: DraggableDimensionMap, + ): DraggableDimension[] => draggableMapToList(draggables) + .filter((draggable: DraggableDimension): boolean => ( + droppable.id === draggable.droppableId + )) + // Dimensions are not guarenteed to be ordered in the same order as keys + // So we need to sort them so they are in the correct order + .sort((a: DraggableDimension, b: DraggableDimension): number => ( + a.page.withoutMargin.center[droppable.axis.line] - + b.page.withoutMargin.center[droppable.axis.line] + )) +); diff --git a/src/state/get-droppable-over.js b/src/state/get-droppable-over.js index d3add29fad..b74504c080 100644 --- a/src/state/get-droppable-over.js +++ b/src/state/get-droppable-over.js @@ -1,13 +1,31 @@ // @flow -import type { DroppableId, Position, DroppableDimensionMap } from '../types'; -import isInsideDroppable from './is-inside-droppable'; +import { droppableMapToList } from './dimension-map-to-list'; +import type { + DroppableId, + Position, + DroppableDimensionMap, + DroppableDimension, + DimensionFragment, +} from '../types'; + +const isOverDroppable = (target: Position, droppable: DroppableDimension): boolean => { + const fragment: DimensionFragment = droppable.page.withMargin; + const { top, right, bottom, left } = fragment; + + return target.x >= left && + target.x <= right && + target.y >= top && + target.y <= bottom; +}; export default ( target: Position, droppables: DroppableDimensionMap, ): ?DroppableId => { - const maybeId: ?DroppableId = Object.keys(droppables) - .find(key => isInsideDroppable(target, droppables[key])); + const maybe: ?DroppableDimension = droppableMapToList(droppables) + .find((droppable: DroppableDimension): boolean => ( + isOverDroppable(target, droppable) + )); - return maybeId || null; + return maybe ? maybe.id : null; }; diff --git a/src/state/is-inside-droppable.js b/src/state/is-inside-droppable.js deleted file mode 100644 index aedd732521..0000000000 --- a/src/state/is-inside-droppable.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow -import type { - Position, - DroppableDimension, - DimensionFragment, -} from '../types'; - -export default (target: Position, dimension: DroppableDimension): boolean => { - const fragment: DimensionFragment = dimension.page.withMargin; - const { top, right, bottom, left } = fragment; - - return target.x >= left && - target.x <= right && - target.y >= top && - target.y <= bottom; -}; diff --git a/src/state/get-best-cross-axis-droppable.js b/src/state/move-to-droppable/get-best-cross-axis-droppable.js similarity index 82% rename from src/state/get-best-cross-axis-droppable.js rename to src/state/move-to-droppable/get-best-cross-axis-droppable.js index e6546b731a..e933654c2c 100644 --- a/src/state/get-best-cross-axis-droppable.js +++ b/src/state/move-to-droppable/get-best-cross-axis-droppable.js @@ -15,19 +15,10 @@ type DistanceToDroppable = {| distance: number, |} -const sortOnCrossAxis = memoizeOne( - (droppables: DroppableDimensionMap, axis: Axis): DroppableDimension[] => - Object.keys(droppables) - .map((key: DroppableId): DroppableDimension => droppables[key]) - .sort((a: DroppableDimension, b: DroppableDimension) => ( - a.page.withMargin[axis.crossAxisStart] - b.page.withMargin[axis.crossAxisStart] - ) - ) -); - const isWithin = (lowerBound: number, upperBound: number): ((number) => boolean) => (value: number): boolean => value <= upperBound && value >= lowerBound; +// TODO: could this be done once and not redone each time? const getCorners = (droppable: DroppableDimension): Position[] => { const fragment: DimensionFragment = droppable.page.withMargin; @@ -42,23 +33,21 @@ const getCorners = (droppable: DroppableDimension): Position[] => { type GetBestDroppableArgs = {| isMovingForward: boolean, center: Position, - droppableId: DroppableId, - droppables: DroppableDimensionMap, + source: DroppableDimension, + droppables: DroppableDimension[], |} export default ({ isMovingForward, center, - droppableId, + source, droppables, }: GetBestDroppableArgs): ?DroppableId => { - const source: DroppableDimension = droppables[droppableId]; const axis: Axis = source.axis; - const sorted: DroppableDimension[] = sortOnCrossAxis(droppables, axis); const candidates: DroppableDimension[] = // 1. Remove the source droppable from the list - sorted.filter((droppable: DroppableDimension): boolean => droppable !== source) + droppables.filter((droppable: DroppableDimension): boolean => droppable !== source) // 2. Get only droppables that are on the desired side .filter((droppable: DroppableDimension): boolean => { if (isMovingForward) { @@ -89,7 +78,11 @@ export default ({ isBetweenDestinationBounds(sourceFragment[axis.start]) || isBetweenDestinationBounds(sourceFragment[axis.end]); }) - // 4. Find the droppables that have the same cross axis value as the first item + // 4. Sort on the cross axis + .sort((a: DroppableDimension, b: DroppableDimension) => ( + a.page.withMargin[axis.crossAxisStart] - b.page.withMargin[axis.crossAxisStart] + )) + // 5. Find the droppables that have the same cross axis value as the first item .filter((droppable: DroppableDimension, index: number, array: DroppableDimension[]): boolean => droppable.page.withMargin[axis.crossAxisStart] === array[0].page.withMargin[axis.crossAxisStart] @@ -119,9 +112,13 @@ export default ({ // to pass every corner than to conditionally grab the right ones distance: closest(center, getCorners(droppable)), })) + // 4. Sort on the cross main axis + .sort((a: DroppableDimension, b: DroppableDimension) => ( + a.page.withMargin[axis.start] - b.page.withMargin[axis.end] + )) // the item with the shortest distance will be first .sort((a: DistanceToDroppable, b: DistanceToDroppable) => a.distance - b.distance) - // TODO: what if there is a tie? + // if there is a tie we return the first - they are already sorted on main axis .map(a => a.id)[0]; return bestId; diff --git a/src/state/move-to-droppable/get-closest-draggable.js b/src/state/move-to-droppable/get-closest-draggable.js new file mode 100644 index 0000000000..936f5ab1d4 --- /dev/null +++ b/src/state/move-to-droppable/get-closest-draggable.js @@ -0,0 +1,27 @@ +// @flow +import { distance } from '../position'; +import type { + Axis, + Position, + DraggableDimension, +} from '../../types'; + +type Args = {| + axis: Axis, + center: Position, + draggables: DraggableDimension[], +|} + +export default ({ + axis, + center, + draggables, +}: Args): DraggableDimension => + draggables.sort((a: DraggableDimension, b: DraggableDimension) => ( + distance(center, a.page.withMargin.center) - + distance(center, b.page.withMargin.center) + )) + // If there is a tie, we want to go into the first slot on the main axis + .sort((a: DraggableDimension, b: DraggableDimension) => ( + a.page.withMargin[axis.start] - b.page.withMargin[axis.start] + ))[0]; diff --git a/src/state/move-to-droppable/index.js b/src/state/move-to-droppable/index.js new file mode 100644 index 0000000000..97d99351ec --- /dev/null +++ b/src/state/move-to-droppable/index.js @@ -0,0 +1,79 @@ +// @flow +import type { + DraggableId, + DroppableId, + Position, + DragImpact, + DroppableDimension, + DraggableDimension, + DraggableDimensionMap, + DroppableDimensionMap, +} from '../../types'; + +type Args = {| + isMovingForward: boolean, + // the current center of the dragging item + center: Position, + // the dragging item + draggableId: DraggableId, + // the droppable the dragging item is in + droppableId: DroppableId, + // all the dimensions in the system + draggables: DraggableDimensionMap, + droppables: DroppableDimensionMap, +|} + +type Result = {| + offset: Position, + impact: DragImpact, +|} + +export default ({ + isMovingForward, + center, + draggableId, + droppableId, + draggables, + droppables, + }): ?Result => { + const draggable: DraggableDimension = draggables[draggableId]; + + const destination: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward, + center, + source: draggable, + droppables, + }); + + // nothing available to move to + if (!destination) { + return null; + } + + const children: DraggableDimension[] = getDraggablesInsideDroppable( + destination, + draggables, + ); + + if (!children.length) { + // need to move to the top of the list + console.info('not handled yet!'); + return null; + } + + const closest: DroppableDimension = getClosestDraggable({ + axis: destination.axis, + center, + draggables: children, + }); + + // TODO: what if going from a vertical to horizontal list + + // needs to go before the closest if it is before / equal on the main axis + const isGoingBefore: boolean = center <= closest.page.withMargin.center[source.axis.line]; + + // isGoingBefore -> bottom edge of current draggable needs go against top edge of closest + // !isGoingBefore -> top of of current draggable needs to go again bottom edge of closest + + // also need to force the other draggables to move to needed +}; diff --git a/src/state/move-to-droppable/move-to-new-spot.js b/src/state/move-to-droppable/move-to-new-spot.js new file mode 100644 index 0000000000..09d476478d --- /dev/null +++ b/src/state/move-to-droppable/move-to-new-spot.js @@ -0,0 +1,14 @@ +// @flow +import type { DraggableDimension } from '../../types'; +// This functions responsibility is to move a draggable above/below a draggable +type Args = {| + source: DraggableDimension, + destination: DraggableDimension, +|} + +export default ({ + source, + destination, +}: Args): Result => { + +}; diff --git a/src/state/reducer.js b/src/state/reducer.js index 7436aa1dee..d09f38c50c 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -233,6 +233,8 @@ export default (state: State = clean('IDLE'), action: Action): State => { center: page.center, }; + console.log('state', state); + const impact: DragImpact = getDragImpact({ page: page.selection, withinDroppable, @@ -435,6 +437,27 @@ export default (state: State = clean('IDLE'), action: Action): State => { }); } + if (action.type === 'CROSS_AXIS_MOVE_FORWARD' || action.type === 'CROSS_AXIS_MOVE_BACKWARD') { + if (state.phase !== 'DRAGGING') { + console.error('cannot move cross axis when not dragging'); + return clean(); + } + + if (!state.drag) { + console.error('cannot move cross axis if there is no drag information'); + return clean(); + } + + if (!state.drag.impact.destination) { + console.error('cannot move cross axis if not in a droppable'); + return clean(); + } + + if (!result) { + return state; + } + } + if (action.type === 'DROP_ANIMATE') { const { trigger, newHomeOffset, impact, result } = action.payload; From 32d2e62305b94681060be43ee08a60209e47c8df Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 08:38:19 +1000 Subject: [PATCH 006/117] action creator plumbing --- src/state/get-best-main-axis-droppable.js | 60 ---------------- .../get-best-cross-axis-droppable.js | 21 +++--- .../get-closest-draggable.js | 9 ++- .../index.js | 24 ++++--- .../move-to-new-spot.js | 0 src/state/reducer.js | 24 +++++-- src/view/drag-handle/drag-handle-types.js | 2 + src/view/drag-handle/drag-handle.jsx | 69 ++++++++++++++----- src/view/draggable/connected-draggable.js | 6 +- src/view/draggable/draggable-types.js | 4 ++ src/view/draggable/draggable.jsx | 16 ++++- 11 files changed, 130 insertions(+), 105 deletions(-) delete mode 100644 src/state/get-best-main-axis-droppable.js rename src/state/{move-to-droppable => move-to-best-droppable}/get-best-cross-axis-droppable.js (88%) rename src/state/{move-to-droppable => move-to-best-droppable}/get-closest-draggable.js (66%) rename src/state/{move-to-droppable => move-to-best-droppable}/index.js (64%) rename src/state/{move-to-droppable => move-to-best-droppable}/move-to-new-spot.js (100%) diff --git a/src/state/get-best-main-axis-droppable.js b/src/state/get-best-main-axis-droppable.js deleted file mode 100644 index 3fa895ad4e..0000000000 --- a/src/state/get-best-main-axis-droppable.js +++ /dev/null @@ -1,60 +0,0 @@ -// @flow -import memoizeOne from 'memoize-one'; -import { closest } from './position'; -import type { - Axis, - Position, - DimensionFragment, - DroppableId, - DroppableDimension, - DroppableDimensionMap, -} from '../types'; - -type DistanceToDroppable = {| - id: DroppableId, - distance: number, -|} - -const sortOnMainAxis = memoizeOne( - (droppables: DroppableDimensionMap, axis: Axis): DroppableDimension[] => - Object.keys(droppables) - .map((key: DroppableId): DroppableDimension => droppables[key]) - .sort((a: DroppableDimension, b: DroppableDimension) => ( - a.page.withMargin[axis.start] - b.page.withMargin[axis.start] - ) - ) -); - -const isWithin = (lowerBound: number, upperBound: number): ((number) => boolean) => - (value: number): boolean => value <= upperBound && value >= lowerBound; - -const getCorners = (droppable: DroppableDimension): Position[] => { - const fragment: DimensionFragment = droppable.page.withMargin; - - return [ - { x: fragment.left, y: fragment.top }, - { x: fragment.right, y: fragment.top }, - { x: fragment.left, y: fragment.bottom }, - { x: fragment.right, y: fragment.bottom }, - ]; -}; - -type GetBestDroppableArgs = {| - isMovingForward: boolean, - center: Position, - droppableId: DroppableId, - droppables: DroppableDimensionMap, -|} - -export default ({ - isMovingForward, - center, - droppableId, - droppables, -}: GetBestDroppableArgs): ?DroppableId => { - const source: DroppableDimension = droppables[droppableId]; - const axis: Axis = source.axis; - const sorted: DroppableDimension[] = sortOnMainAxis(droppables, axis); - - // ... -}; diff --git a/src/state/move-to-droppable/get-best-cross-axis-droppable.js b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js similarity index 88% rename from src/state/move-to-droppable/get-best-cross-axis-droppable.js rename to src/state/move-to-best-droppable/get-best-cross-axis-droppable.js index e933654c2c..7adb9e1379 100644 --- a/src/state/move-to-droppable/get-best-cross-axis-droppable.js +++ b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js @@ -1,6 +1,6 @@ // @flow -import memoizeOne from 'memoize-one'; -import { closest } from './position'; +import { closest } from '../position'; +import { droppableMapToList } from '../dimension-map-to-list'; import type { Axis, Position, @@ -8,7 +8,7 @@ import type { DroppableId, DroppableDimension, DroppableDimensionMap, -} from '../types'; +} from '../../types'; type DistanceToDroppable = {| id: DroppableId, @@ -32,9 +32,12 @@ const getCorners = (droppable: DroppableDimension): Position[] => { type GetBestDroppableArgs = {| isMovingForward: boolean, + // the current position of the dragging item center: Position, + // the home of the draggable source: DroppableDimension, - droppables: DroppableDimension[], + // all the droppables in the system + droppables: DroppableDimensionMap, |} export default ({ @@ -45,9 +48,9 @@ export default ({ }: GetBestDroppableArgs): ?DroppableId => { const axis: Axis = source.axis; - const candidates: DroppableDimension[] = + const candidates: DroppableDimension[] = droppableMapToList(droppables) // 1. Remove the source droppable from the list - droppables.filter((droppable: DroppableDimension): boolean => droppable !== source) + .filter((droppable: DroppableDimension): boolean => droppable !== source) // 2. Get only droppables that are on the desired side .filter((droppable: DroppableDimension): boolean => { if (isMovingForward) { @@ -107,13 +110,13 @@ export default ({ // 3. in the event of a tie: choose the corner that is closest to {x: 0, y: 0} const bestId: DroppableId = candidates.map((droppable: DroppableDimension): DistanceToDroppable => ({ - id: droppableId, + id: droppable.id, // two of the corners will be redundant, but it is *way* easier // to pass every corner than to conditionally grab the right ones distance: closest(center, getCorners(droppable)), })) - // 4. Sort on the cross main axis - .sort((a: DroppableDimension, b: DroppableDimension) => ( + // 4. Sort on the main axis + .sort((a: DistanceToDroppable, b: DistanceToDroppable) => ( a.page.withMargin[axis.start] - b.page.withMargin[axis.end] )) // the item with the shortest distance will be first diff --git a/src/state/move-to-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js similarity index 66% rename from src/state/move-to-droppable/get-closest-draggable.js rename to src/state/move-to-best-droppable/get-closest-draggable.js index 936f5ab1d4..fddaa87d57 100644 --- a/src/state/move-to-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -1,5 +1,5 @@ // @flow -import { distance } from '../position'; +import { add, distance } from '../position'; import type { Axis, Position, @@ -9,17 +9,20 @@ import type { type Args = {| axis: Axis, center: Position, + // how far the destination Droppable is scrolled + scrollOffset: Position, draggables: DraggableDimension[], |} export default ({ axis, center, + scrollOffset, draggables, }: Args): DraggableDimension => draggables.sort((a: DraggableDimension, b: DraggableDimension) => ( - distance(center, a.page.withMargin.center) - - distance(center, b.page.withMargin.center) + distance(center, add(a.page.withMargin.center, scrollOffset)) - + distance(center, add(b.page.withMargin.center, scrollOffset)) )) // If there is a tie, we want to go into the first slot on the main axis .sort((a: DraggableDimension, b: DraggableDimension) => ( diff --git a/src/state/move-to-droppable/index.js b/src/state/move-to-best-droppable/index.js similarity index 64% rename from src/state/move-to-droppable/index.js rename to src/state/move-to-best-droppable/index.js index 97d99351ec..22daf7bb80 100644 --- a/src/state/move-to-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -1,4 +1,7 @@ // @flow +import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; +import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; +import getClosestDraggable from './get-closest-draggable'; import type { DraggableId, DroppableId, @@ -35,13 +38,14 @@ export default ({ droppableId, draggables, droppables, - }): ?Result => { + }: Args): ?Result => { const draggable: DraggableDimension = draggables[draggableId]; + const source: DroppableDimension = droppables[droppableId]; const destination: ?DroppableDimension = getBestCrossAxisDroppable({ isMovingForward, center, - source: draggable, + source, droppables, }); @@ -50,27 +54,31 @@ export default ({ return null; } - const children: DraggableDimension[] = getDraggablesInsideDroppable( + const newSiblings: DraggableDimension[] = getDraggablesInsideDroppable( destination, draggables, ); - if (!children.length) { - // need to move to the top of the list + if (!newSiblings.length) { + // need to move to the start of the list console.info('not handled yet!'); return null; } - const closest: DroppableDimension = getClosestDraggable({ + // Assumption: list must have same width + // All good if smaller - but if bigger then it will be a bit messy - up to consumer + + const closestSibling: DroppableDimension = getClosestDraggable({ axis: destination.axis, center, - draggables: children, + scrollOffset: destination.scroll.current, + draggables: newSiblings, }); // TODO: what if going from a vertical to horizontal list // needs to go before the closest if it is before / equal on the main axis - const isGoingBefore: boolean = center <= closest.page.withMargin.center[source.axis.line]; + const isGoingBefore: boolean = center <= closestSibling.page.withMargin.center[source.axis.line]; // isGoingBefore -> bottom edge of current draggable needs go against top edge of closest // !isGoingBefore -> top of of current draggable needs to go again bottom edge of closest diff --git a/src/state/move-to-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js similarity index 100% rename from src/state/move-to-droppable/move-to-new-spot.js rename to src/state/move-to-best-droppable/move-to-new-spot.js diff --git a/src/state/reducer.js b/src/state/reducer.js index d09f38c50c..e2ab38550a 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -6,6 +6,7 @@ import type { TypeId, DraggableDimension, DroppableDimension, DroppableId, + DraggableId, DimensionState, DragImpact, DragState, @@ -24,6 +25,7 @@ import getDragImpact from './get-drag-impact'; import jumpToNextIndex from './jump-to-next-index'; import type { JumpToNextResult } from './jump-to-next-index'; import getDroppableOver from './get-droppable-over'; +import moveToBestDroppable from './move-to-best-droppable/'; const noDimensions: DimensionState = { request: null, @@ -233,8 +235,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { center: page.center, }; - console.log('state', state); - const impact: DragImpact = getDragImpact({ page: page.selection, withinDroppable, @@ -438,6 +438,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { } if (action.type === 'CROSS_AXIS_MOVE_FORWARD' || action.type === 'CROSS_AXIS_MOVE_BACKWARD') { + console.log('trying to moving on cross axis', action.type); if (state.phase !== 'DRAGGING') { console.error('cannot move cross axis when not dragging'); return clean(); @@ -453,9 +454,22 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } - if (!result) { - return state; - } + const draggableId: DraggableId = state.drag.current.id; + const center: Position = state.drag.current.page.center; + const droppableId: DroppableId = state.drag.impact.destination.droppableId; + + const result = moveToBestDroppable({ + isMovingForward: action.type === 'CROSS_AXIS_MOVE_FORWARD', + center, + draggableId, + droppableId, + draggables: state.dimension.draggable, + droppables: state.dimension.droppable, + }); + + console.log('result', result); + + return state; } if (action.type === 'DROP_ANIMATE') { diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/drag-handle/drag-handle-types.js index ce4bca4ff1..2d86e8c002 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/drag-handle/drag-handle-types.js @@ -9,6 +9,8 @@ export type Callbacks = {| onWindowScroll: (diff: Position) => void, onMoveForward: () => void, onMoveBackward: () => void, + onCrossAxisMoveForward: () => void, + onCrossAxisMoveBackward: () => void, onDrop: () => void, onCancel: () => void, |} diff --git a/src/view/drag-handle/drag-handle.jsx b/src/view/drag-handle/drag-handle.jsx index ce9b8f8b19..0ee1f6e7c2 100644 --- a/src/view/drag-handle/drag-handle.jsx +++ b/src/view/drag-handle/drag-handle.jsx @@ -2,7 +2,7 @@ import { Component } from 'react'; import invariant from 'invariant'; import memoizeOne from 'memoize-one'; -import rafScheduler from 'raf-schd'; +import rafSchedule from 'raf-schd'; // Using keyCode's for consistent event pattern matching between // React synthetic events as well as raw browser events. import * as keyCodes from '../key-codes'; @@ -24,6 +24,11 @@ type State = { pending: ?Position, }; +type ExecuteBasedOnDirection = {| + vertical: Function, + horizontal: Function, +|} + export default class DragHandle extends Component { /* eslint-disable react/sort-comp */ @@ -55,19 +60,27 @@ export default class DragHandle extends Component { }); // scheduled functions - scheduleMove = rafScheduler((point: Position) => { + scheduleMove = rafSchedule((point: Position) => { this.ifDragging(() => this.memoizedMove(point.x, point.y)); }); - scheduleMoveForward = rafScheduler(() => { + scheduleMoveForward = rafSchedule(() => { this.ifDragging(this.props.callbacks.onMoveForward); }) - scheduleMoveBackward = rafScheduler(() => { + scheduleMoveBackward = rafSchedule(() => { this.ifDragging(this.props.callbacks.onMoveBackward); }); - scheduleWindowScrollMove = rafScheduler(() => { + scheduleCrossAxisMoveForward = rafSchedule(() => { + this.ifDragging(this.props.callbacks.onCrossAxisMoveForward); + }) + + scheduleCrossAxisMoveBackward = rafSchedule(() => { + this.ifDragging(this.props.callbacks.onCrossAxisMoveBackward); + }); + + scheduleWindowScrollMove = rafSchedule(() => { this.ifDragging(this.props.callbacks.onWindowScroll); }); /* eslint-enable react/sort-comp */ @@ -232,6 +245,17 @@ export default class DragHandle extends Component { this.startPendingMouseDrag(point); }; + executeBasedOnDirection = ({ vertical, horizontal }: Object) => { + if (!this.props.direction) { + console.error('cannot move based on direction when none is provided'); + this.stopDragging(() => this.props.callbacks.onCancel()); + return; + } + + // eslint-disable-next-line no-unused-expressions + this.props.direction === 'vertical' ? vertical() : horizontal(); + } + // window keyboard events are bound during a keyboard drag // or after the user presses the mouse down onWindowKeydown = (event: KeyboardEvent): void => { @@ -293,30 +317,39 @@ export default class DragHandle extends Component { return; } - if (this.props.direction === 'vertical') { - if (event.keyCode === keyCodes.arrowDown) { - event.preventDefault(); - this.scheduleMoveForward(); - } - - if (event.keyCode === keyCodes.arrowUp) { - event.preventDefault(); - this.scheduleMoveBackward(); - } + if (event.keyCode === keyCodes.arrowDown) { + event.preventDefault(); + this.executeBasedOnDirection({ + vertical: this.scheduleMoveForward, + horizontal: this.scheduleCrossAxisMoveForward, + }); + return; + } + if (event.keyCode === keyCodes.arrowUp) { + event.preventDefault(); + this.executeBasedOnDirection({ + vertical: this.scheduleMoveBackward, + horizontal: this.scheduleCrossAxisMoveBackward, + }); return; } - // horizontal dragging if (event.keyCode === keyCodes.arrowRight) { event.preventDefault(); - this.scheduleMoveForward(); + this.executeBasedOnDirection({ + vertical: this.scheduleCrossAxisMoveForward, + horizontal: this.scheduleMoveForward, + }); return; } if (event.keyCode === keyCodes.arrowLeft) { event.preventDefault(); - this.scheduleMoveBackward(); + this.executeBasedOnDirection({ + vertical: this.scheduleCrossAxisMoveBackward, + horizontal: this.scheduleMoveBackward, + }); } } diff --git a/src/view/draggable/connected-draggable.js b/src/view/draggable/connected-draggable.js index 36bd200499..1be404379c 100644 --- a/src/view/draggable/connected-draggable.js +++ b/src/view/draggable/connected-draggable.js @@ -15,6 +15,8 @@ import { move as moveAction, moveForward as moveForwardAction, moveBackward as moveBackwardAction, + crossAxisMoveForward as crossAxisMoveForwardAction, + crossAxisMoveBackward as crossAxisMoveBackwardAction, drop as dropAction, cancel as cancelAction, dropAnimationFinished as dropAnimationFinishedAction, @@ -227,8 +229,10 @@ const makeMapStateToProps = () => { const mapDispatchToProps: DispatchProps = { lift: liftAction, move: moveAction, - moveBackward: moveBackwardAction, moveForward: moveForwardAction, + moveBackward: moveBackwardAction, + crossAxisMoveForward: crossAxisMoveForwardAction, + crossAxisMoveBackward: crossAxisMoveBackwardAction, moveByWindowScroll: moveByWindowScrollAction, drop: dropAction, dropAnimationFinished: dropAnimationFinishedAction, diff --git a/src/view/draggable/draggable-types.js b/src/view/draggable/draggable-types.js index 006c79190c..0c20c8125f 100644 --- a/src/view/draggable/draggable-types.js +++ b/src/view/draggable/draggable-types.js @@ -20,6 +20,8 @@ import { moveByWindowScroll, moveForward, moveBackward, + crossAxisMoveForward, + crossAxisMoveBackward, drop, cancel, dropAnimationFinished, @@ -98,6 +100,8 @@ export type DispatchProps = { moveByWindowScroll: PropType, moveForward: PropType, moveBackward: PropType, + crossAxisMoveForward: PropType, + crossAxisMoveBackward: PropType, drop: PropType, cancel: PropType, dropAnimationFinished: PropType, diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 661727400f..559a7870f2 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -69,7 +69,7 @@ export default class Draggable extends Component { constructor(props: Props, context: mixed) { super(props, context); - this.callbacks = { + const callbacks: DragHandleCallbacks = { onLift: this.onLift, onMove: this.onMove, onDrop: this.onDrop, @@ -77,8 +77,12 @@ export default class Draggable extends Component { onKeyLift: this.onKeyLift, onMoveBackward: this.onMoveBackward, onMoveForward: this.onMoveForward, + onCrossAxisMoveForward: this.onCrossAxisMoveForward, + onCrossAxisMoveBackward: this.onCrossAxisMoveBackward, onWindowScroll: this.onWindowScroll, }; + + this.callbacks = callbacks; } // This should already be handled gracefully in DragHandle. @@ -168,6 +172,16 @@ export default class Draggable extends Component { this.props.moveBackward(this.props.draggableId); } + onCrossAxisMoveForward = () => { + this.throwIfCannotDrag(); + this.props.crossAxisMoveForward(this.props.draggableId); + } + + onCrossAxisMoveBackward = () => { + this.throwIfCannotDrag(); + this.props.crossAxisMoveBackward(this.props.draggableId); + } + onWindowScroll = () => { this.throwIfCannotDrag(); const windowScroll = getWindowScrollPosition(); From ee6a4de32c8e5a77456eab47777ff79bbec3365b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 09:20:34 +1000 Subject: [PATCH 007/117] more refinement --- .../get-best-cross-axis-droppable.js | 34 +++++++++++-------- .../get-closest-draggable.js | 26 +++++++++----- src/state/move-to-best-droppable/index.js | 17 ++++++---- src/view/drag-handle/drag-handle.jsx | 4 +-- stories/5-multiple-vertical-lists-story.js | 11 +++--- stories/src/multiple-vertical/quote-app.js | 21 ++++++++---- 6 files changed, 69 insertions(+), 44 deletions(-) diff --git a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js index 7adb9e1379..fe10ae68c5 100644 --- a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js +++ b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js @@ -11,7 +11,7 @@ import type { } from '../../types'; type DistanceToDroppable = {| - id: DroppableId, + droppable: DroppableDimension, distance: number, |} @@ -45,7 +45,7 @@ export default ({ center, source, droppables, -}: GetBestDroppableArgs): ?DroppableId => { +}: GetBestDroppableArgs): ?DroppableDimension => { const axis: Axis = source.axis; const candidates: DroppableDimension[] = droppableMapToList(droppables) @@ -98,7 +98,7 @@ export default ({ // only one result - all done! if (candidates.length === 1) { - return candidates[0].id; + return candidates[0]; } // At this point we have a number of candidates that @@ -108,21 +108,27 @@ export default ({ // 1. Get the distance to all of the corner points // 2. Find the closest corner to current center // 3. in the event of a tie: choose the corner that is closest to {x: 0, y: 0} - const bestId: DroppableId = + const best: DroppableDimension = candidates.map((droppable: DroppableDimension): DistanceToDroppable => ({ - id: droppable.id, + droppable, // two of the corners will be redundant, but it is *way* easier // to pass every corner than to conditionally grab the right ones distance: closest(center, getCorners(droppable)), })) - // 4. Sort on the main axis - .sort((a: DistanceToDroppable, b: DistanceToDroppable) => ( - a.page.withMargin[axis.start] - b.page.withMargin[axis.end] - )) - // the item with the shortest distance will be first - .sort((a: DistanceToDroppable, b: DistanceToDroppable) => a.distance - b.distance) - // if there is a tie we return the first - they are already sorted on main axis - .map(a => a.id)[0]; + .sort((a: DistanceToDroppable, b: DistanceToDroppable): number => { + // 'a' is closer - make 'a' first + if (a.distance < b.distance) { + return -1; + } + + // 'b' is closer - make 'b' first + if (b.distance > a.distance) { + return 1; + } + + // they have equal distances - sort based on which appears first on the main axis + return a.droppable.page.withMargin[axis.start] - b.droppable.page.withMargin[axis.start]; + })[0].droppable; - return bestId; + return best; }; diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index fddaa87d57..93b57e4649 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -20,11 +20,21 @@ export default ({ scrollOffset, draggables, }: Args): DraggableDimension => - draggables.sort((a: DraggableDimension, b: DraggableDimension) => ( - distance(center, add(a.page.withMargin.center, scrollOffset)) - - distance(center, add(b.page.withMargin.center, scrollOffset)) - )) - // If there is a tie, we want to go into the first slot on the main axis - .sort((a: DraggableDimension, b: DraggableDimension) => ( - a.page.withMargin[axis.start] - b.page.withMargin[axis.start] - ))[0]; + draggables.sort((a: DraggableDimension, b: DraggableDimension): number => { + const distanceToA = distance(center, add(a.page.withMargin.center, scrollOffset)); + const distanceToB = distance(center, add(b.page.withMargin.center, scrollOffset)); + + // if a is closer - return a + if (distanceToA > distanceToB) { + return -1; + } + + // if b is closer - return b + if (distanceToB < distanceToA) { + return 1; + } + + // if the distance to a and b are the same: + // return the one that appears first on the main axis + return a.page.withMargin[axis.start] - b.page.withMargin[axis.start]; + })[0]; diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index 22daf7bb80..4ab2365d67 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -39,7 +39,6 @@ export default ({ draggables, droppables, }: Args): ?Result => { - const draggable: DraggableDimension = draggables[draggableId]; const source: DroppableDimension = droppables[droppableId]; const destination: ?DroppableDimension = getBestCrossAxisDroppable({ @@ -49,8 +48,11 @@ export default ({ droppables, }); + console.log('desintation', destination); + // nothing available to move to if (!destination) { + console.log('no destination found'); return null; } @@ -59,6 +61,8 @@ export default ({ draggables, ); + console.log('new siblings', newSiblings); + if (!newSiblings.length) { // need to move to the start of the list console.info('not handled yet!'); @@ -68,20 +72,21 @@ export default ({ // Assumption: list must have same width // All good if smaller - but if bigger then it will be a bit messy - up to consumer - const closestSibling: DroppableDimension = getClosestDraggable({ + const closestSibling: DraggableDimension = getClosestDraggable({ axis: destination.axis, center, scrollOffset: destination.scroll.current, draggables: newSiblings, }); + console.log('closest', closestSibling); + // TODO: what if going from a vertical to horizontal list // needs to go before the closest if it is before / equal on the main axis - const isGoingBefore: boolean = center <= closestSibling.page.withMargin.center[source.axis.line]; - - // isGoingBefore -> bottom edge of current draggable needs go against top edge of closest - // !isGoingBefore -> top of of current draggable needs to go again bottom edge of closest + // Keep in mind that an item 'before' another will have a smaller value on the viewport + const isGoingBefore: boolean = center[source.axis.line] < closestSibling.page.withMargin.center[source.axis.line]; // also need to force the other draggables to move to needed + console.log('is going before?', isGoingBefore); }; diff --git a/src/view/drag-handle/drag-handle.jsx b/src/view/drag-handle/drag-handle.jsx index 0ee1f6e7c2..934771f931 100644 --- a/src/view/drag-handle/drag-handle.jsx +++ b/src/view/drag-handle/drag-handle.jsx @@ -25,8 +25,8 @@ type State = { }; type ExecuteBasedOnDirection = {| - vertical: Function, - horizontal: Function, + vertical: () => void, + horizontal: () => void, |} export default class DragHandle extends Component { diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js index 8d2a5cb3e0..e60034169b 100644 --- a/stories/5-multiple-vertical-lists-story.js +++ b/stories/5-multiple-vertical-lists-story.js @@ -2,7 +2,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import QuoteApp from './src/multiple-vertical/quote-app'; -import { quotes } from './src/data'; +import { getQuotes } from './src/data'; const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( quote => ({ @@ -11,13 +11,10 @@ const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( }) ); -// I don't want these to be random -const alphaQuotes = quotes.slice(0, 2); -const betaQuotes = quotes.slice(6, 8); - const initialQuotes = { - alpha: namespaceQuoteIds(alphaQuotes, 'alpha'), - beta: namespaceQuoteIds(betaQuotes, 'beta'), + alpha: namespaceQuoteIds(getQuotes(10), 'alpha'), + beta: namespaceQuoteIds(getQuotes(3), 'beta'), + gamma: namespaceQuoteIds(getQuotes(10), 'gamma'), }; storiesOf('multiple vertical lists', module) diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js index f5a928e486..235b21914b 100644 --- a/stories/src/multiple-vertical/quote-app.js +++ b/stories/src/multiple-vertical/quote-app.js @@ -7,7 +7,7 @@ import QuoteList from './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, DraggableLocation } from '../../../src/types'; const publishOnDragStart = action('onDragStart'); const publishOnDragEnd = action('onDragEnd'); @@ -26,22 +26,23 @@ const Root = styled.div` const isDraggingClassName = 'is-dragging'; -type Quotes = { +type GroupedQuotes = { alpha: Quote[], beta: Quote[], + gamma: Quote[], } type Props = {| - initial: Quotes, + initial: GroupedQuotes, listStyle?: Object, |} type State = {| - quotes: Quotes, + quotes: GroupedQuotes, |} -const resolveDrop = (quotes: Quotes, { source, destination }): Quotes => { - const newQuotes: Quotes = { ...quotes }; +const resolveDrop = (quotes: GroupedQuotes, source: DraggableLocation, destination: DraggableLocation): GroupedQuotes => { + const newQuotes: GroupedQuotes = { ...quotes }; const movedQuote = quotes[source.droppableId][source.index]; @@ -95,7 +96,7 @@ export default class QuoteApp extends Component { return; } - const quotes = resolveDrop(this.state.quotes, result); + const quotes = resolveDrop(this.state.quotes, result.source, result.destination); this.setState({ quotes }); } @@ -135,6 +136,12 @@ export default class QuoteApp extends Component { style={style} quotes={quotes.beta} /> + ); From 4ca11acb6aee03f24601ef4eb23577799dc7fe4a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 09:43:14 +1000 Subject: [PATCH 008/117] adding complexity to stories --- stories/5-multiple-vertical-lists-story.js | 1 + stories/src/multiple-vertical/quote-app.js | 53 ++++++++++++++------- stories/src/multiple-vertical/quote-list.js | 5 ++ 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js index e60034169b..8e11c238e4 100644 --- a/stories/5-multiple-vertical-lists-story.js +++ b/stories/5-multiple-vertical-lists-story.js @@ -15,6 +15,7 @@ const initialQuotes = { alpha: namespaceQuoteIds(getQuotes(10), 'alpha'), beta: namespaceQuoteIds(getQuotes(3), 'beta'), gamma: namespaceQuoteIds(getQuotes(10), 'gamma'), + delta: namespaceQuoteIds(getQuotes(5), 'delta'), }; storiesOf('multiple vertical lists', module) diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js index 235b21914b..94bd13d28c 100644 --- a/stories/src/multiple-vertical/quote-app.js +++ b/stories/src/multiple-vertical/quote-app.js @@ -24,12 +24,17 @@ const Root = styled.div` align-items: flex-start; `; +const Column = styled.div` + +`; + const isDraggingClassName = 'is-dragging'; type GroupedQuotes = { alpha: Quote[], beta: Quote[], gamma: Quote[], + delta: Quote[], } type Props = {| @@ -124,24 +129,36 @@ export default class QuoteApp extends Component { onDragEnd={this.onDragEnd} > - - - + + + + + + + + + + ); diff --git a/stories/src/multiple-vertical/quote-list.js b/stories/src/multiple-vertical/quote-list.js index d78cceb237..b8faca26f2 100644 --- a/stories/src/multiple-vertical/quote-list.js +++ b/stories/src/multiple-vertical/quote-list.js @@ -25,6 +25,10 @@ const Container = styled.div` width: 250px; `; +const Title = styled.h4` + margin-bottom: ${grid}px; +`; + export default class QuoteList extends Component { props: {| listId: string, @@ -43,6 +47,7 @@ export default class QuoteList extends Component { innerRef={dropProvided.innerRef} style={style} > + {listId} {quotes.map((quote: Quote) => ( {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( From 545a9aeab685430de73f8f4621a8745cdc660331 Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Mon, 28 Aug 2017 10:45:37 +1000 Subject: [PATCH 009/117] WIP --- src/state/action-creators.js | 3 +- src/state/get-drag-impact.js | 14 ++++++- src/state/get-new-home-client-offset.js | 44 +++++++++++++++------- src/state/position.js | 11 ++++-- stories/5-multiple-vertical-lists-story.js | 4 +- 5 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 8fd589ccc0..77c78bb926 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -17,7 +17,7 @@ import type { } from '../types'; import noImpact from './no-impact'; import getNewHomeClientOffset from './get-new-home-client-offset'; -import { add, subtract, isEqual } from './position'; +import { add, subtract, isEqual, patch } from './position'; const origin: Position = { x: 0, y: 0 }; @@ -296,6 +296,7 @@ export const drop = () => windowScrollDiff: scrollDiff.window, draggables: state.dimension.draggable, axis: destinationDroppable ? destinationDroppable.axis : null, + draggableId: current.id, }); // Do not animate if you do not need to. diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 1bab8637a0..bf47986e69 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -60,6 +60,8 @@ export default ({ // not considering margin so that items move based on visible edges const draggableCenter: Position = draggingDimension.page.withoutMargin.center; const isBeyondStartPosition: boolean = newCenter[axis.line] - draggableCenter[axis.line] > 0; + const isWithinHomeDroppable = draggingDimension.droppableId === droppableId; + const shouldDisplaceItemsForward = isWithinHomeDroppable ? isBeyondStartPosition : false; const moved: DraggableId[] = insideDroppable .filter((dimension: DraggableDimension): boolean => { @@ -70,6 +72,12 @@ export default ({ const fragment: DimensionFragment = dimension.page.withoutMargin; + // If we're over a new droppable items will be displaced + // if they sit ahead of the dragging item + if (!isWithinHomeDroppable) { + return newCenter[axis.line] < fragment[axis.end]; + } + if (isBeyondStartPosition) { // 1. item needs to start ahead of the moving item // 2. the dragging item has moved over it @@ -92,6 +100,10 @@ export default ({ const startIndex = insideDroppable.indexOf(draggingDimension); const index: number = (() => { + if (!isWithinHomeDroppable) { + return insideDroppable.length - moved.length; + } + if (!moved.length) { return startIndex; } @@ -113,7 +125,7 @@ export default ({ const movement: DragMovement = { amount, draggables: moved, - isBeyondStartPosition, + isBeyondStartPosition: shouldDisplaceItemsForward, }; const impact: DragImpact = { diff --git a/src/state/get-new-home-client-offset.js b/src/state/get-new-home-client-offset.js index 7bb3ad5eaf..15458f6daa 100644 --- a/src/state/get-new-home-client-offset.js +++ b/src/state/get-new-home-client-offset.js @@ -2,7 +2,7 @@ import type { DragMovement, Position, - DraggableDimension, + DimensionFragment, DraggableDimensionMap, DraggableId, Axis, @@ -13,6 +13,7 @@ type NewHomeArgs = {| movement: DragMovement, clientOffset: Position, pageOffset: Position, + draggableId: DraggableId, droppableScrollDiff: Position, windowScrollDiff: Position, draggables: DraggableDimensionMap, @@ -28,13 +29,16 @@ export default ({ movement, clientOffset, pageOffset, + draggableId, droppableScrollDiff, windowScrollDiff, draggables, axis, }: NewHomeArgs): ClientOffset => { + const { draggables: movedDraggables, isBeyondStartPosition } = movement; + // Just animate back to where it started - if (!movement.draggables.length) { + if (!movedDraggables.length) { return add(droppableScrollDiff, windowScrollDiff); } @@ -43,19 +47,33 @@ export default ({ return add(droppableScrollDiff, windowScrollDiff); } - // Currently not considering horizontal movement - const distance: number = movement.draggables.reduce( - (previous: number, draggableId: DraggableId): number => { - const dimension: DraggableDimension = draggables[draggableId]; - // $ExpectError - for some reason flow is not liking axis.size - return previous + dimension.page.withMargin[axis.size]; - }, 0); + // The dimension of the item being dragged + const draggedDimension: DimensionFragment = draggables[draggableId].client.withoutMargin; + // The index of the last item being displaced + const displacedIndex: number = isBeyondStartPosition ? movedDraggables.length - 1 : 0; + // The dimension of the last item being displaced + const displacedDimension: DimensionFragment = draggables[ + movedDraggables[displacedIndex] + ].client.withoutMargin; + + // Find the difference between the center of the dragging item + // and the center of the last item being displaced + const distanceToDisplacedCenter: Position = subtract( + displacedDimension.center, + draggedDimension.center + ); - const signed: number = movement.isBeyondStartPosition ? distance : -distance; + // To account for items of different sizes we need to adjust the offset + // between their center points by half their size difference + const mainAxisSizeDifference: number = ( + ((draggedDimension[axis.size] - displacedDimension[axis.size]) + / 2) + * (isBeyondStartPosition ? -1 : 1) + ); + const mainAxisSizeOffset: Position = patch(axis.line, mainAxisSizeDifference); - // How much distance the item needs to travel to be in its new home - // from where it started - const amount = patch(axis.line, signed); + // Finally, this is how far the dragged item has to travel to be in its new home + const amount: Position = add(distanceToDisplacedCenter, mainAxisSizeOffset); // How far away it is on the page from where it needs to be const diff: Position = subtract(amount, pageOffset); diff --git a/src/state/position.js b/src/state/position.js index 045d4307f9..1f272353c7 100644 --- a/src/state/position.js +++ b/src/state/position.js @@ -20,8 +20,11 @@ export const negate = (point: Position): Position => ({ y: point.y !== 0 ? -point.y : 0, }); -export const patch = (line: 'x' | 'y', value: number): Position => ({ - x: line === 'x' ? value : 0, - y: line === 'y' ? value : 0, +export const patch = ( + line: 'x' | 'y', + value: number, + otherValue?: number = 0 +): Position => ({ + [line]: value, + [line === 'x' ? 'y' : 'x']: otherValue, }); - diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js index 8d2a5cb3e0..6ef4c2ec3e 100644 --- a/stories/5-multiple-vertical-lists-story.js +++ b/stories/5-multiple-vertical-lists-story.js @@ -12,8 +12,8 @@ const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( ); // I don't want these to be random -const alphaQuotes = quotes.slice(0, 2); -const betaQuotes = quotes.slice(6, 8); +const alphaQuotes = quotes.slice(0, 4); +const betaQuotes = quotes.slice(5, 9); const initialQuotes = { alpha: namespaceQuoteIds(alphaQuotes, 'alpha'), From 27e6694a864b725b55b948a8e6309e8e7694d14f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 11:16:51 +1000 Subject: [PATCH 010/117] improving get-best-cross-axis-droppable --- .../get-best-cross-axis-droppable.js | 82 +++++++++++-------- stories/src/multiple-vertical/quote-app.js | 5 ++ test/unit/state/position.spec.js | 17 ++++ 3 files changed, 69 insertions(+), 35 deletions(-) diff --git a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js index fe10ae68c5..698aa5f52c 100644 --- a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js +++ b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js @@ -5,20 +5,13 @@ import type { Axis, Position, DimensionFragment, - DroppableId, DroppableDimension, DroppableDimensionMap, } from '../../types'; -type DistanceToDroppable = {| - droppable: DroppableDimension, - distance: number, -|} - const isWithin = (lowerBound: number, upperBound: number): ((number) => boolean) => (value: number): boolean => value <= upperBound && value >= lowerBound; -// TODO: could this be done once and not redone each time? const getCorners = (droppable: DroppableDimension): Position[] => { const fragment: DimensionFragment = droppable.page.withMargin; @@ -82,9 +75,15 @@ export default ({ isBetweenDestinationBounds(sourceFragment[axis.end]); }) // 4. Sort on the cross axis - .sort((a: DroppableDimension, b: DroppableDimension) => ( - a.page.withMargin[axis.crossAxisStart] - b.page.withMargin[axis.crossAxisStart] - )) + .sort((a: DroppableDimension, b: DroppableDimension) => { + const first: number = a.page.withMargin[axis.crossAxisStart]; + const second: number = b.page.withMargin[axis.crossAxisStart]; + + if (isMovingForward) { + return first - second; + } + return second - first; + }) // 5. Find the droppables that have the same cross axis value as the first item .filter((droppable: DroppableDimension, index: number, array: DroppableDimension[]): boolean => droppable.page.withMargin[axis.crossAxisStart] === @@ -103,32 +102,45 @@ export default ({ // At this point we have a number of candidates that // all have the same axis.crossAxisStart value. - // Now need to consider the main axis as a tiebreaker - - // 1. Get the distance to all of the corner points - // 2. Find the closest corner to current center - // 3. in the event of a tie: choose the corner that is closest to {x: 0, y: 0} - const best: DroppableDimension = - candidates.map((droppable: DroppableDimension): DistanceToDroppable => ({ - droppable, - // two of the corners will be redundant, but it is *way* easier - // to pass every corner than to conditionally grab the right ones - distance: closest(center, getCorners(droppable)), - })) - .sort((a: DistanceToDroppable, b: DistanceToDroppable): number => { - // 'a' is closer - make 'a' first - if (a.distance < b.distance) { - return -1; - } - // 'b' is closer - make 'b' first - if (b.distance > a.distance) { - return 1; - } + // 1. Check to see if the center position is within the size of a Droppable on the main axis + + const contains: DroppableDimension[] = candidates + .filter((droppable: DroppableDimension) => { + const isWithinDroppable = isWithin( + droppable.page.withMargin[axis.start], + droppable.page.withMargin[axis.end] + ); + return isWithinDroppable(center[axis.line]); + }); + + if (contains.length === 1) { + return contains[0]; + } + + // the center point of the draggable falls on the boundary between to droppables + if (contains.length > 1) { + // sort on the main axis and choose the first + return contains.sort((a: DroppableDimension, b: DroppableDimension) => ( + a.page.withMargin[axis.start] - b.page.withMargin[axis.start] + ))[0]; + } + + // 2. The center is not contained within any droppable + + // 1. Find the candidate that has the closest corner + // 2. If there is a tie - choose the one that is first on the main axis + return candidates.sort((a: DroppableDimension, b: DroppableDimension) => { + const first = closest(center, getCorners(a)); + const second = closest(center, getCorners(b)); - // they have equal distances - sort based on which appears first on the main axis - return a.droppable.page.withMargin[axis.start] - b.droppable.page.withMargin[axis.start]; - })[0].droppable; + // if the distances are not equal - choose the shortest + if (first !== second) { + return first - second; + } - return best; + // they both have the same distance - + // choose the one that is first on the main axis + return a.page.withMargin[axis.start] - b.page.withMargin[axis.start]; + })[0]; }; diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js index 94bd13d28c..1641123922 100644 --- a/stories/src/multiple-vertical/quote-app.js +++ b/stories/src/multiple-vertical/quote-app.js @@ -28,6 +28,10 @@ const Column = styled.div` `; +const PushDown = styled.div` + height: 400px; +`; + const isDraggingClassName = 'is-dragging'; type GroupedQuotes = { @@ -138,6 +142,7 @@ export default class QuoteApp extends Component { /> + { const target = { x: 3, y: 4 }; expect(distance(origin, target)).toEqual(5); }); + + it('should account for a negative shift in plane', () => { + // a reverse '3, 4, 5' triangle shifted down to (-1, -1) + const customOrigin = { x: -1, y: -1 }; + const target = { x: -4, y: -5 }; + expect(distance(customOrigin, target)).toEqual(5); + }); + }); + }); + + describe('closest', () => { + it('should return the closest distance from a series of options', () => { + const option1 = { x: 1, y: 1 }; + const option2 = { x: 2, y: 2 }; + + expect(closest(origin, [option1, option2])).toEqual(distance(origin, option1)); }); }); }); From 095e23e9c432e8eb03d94132b453832de2fbfce8 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 11:46:15 +1000 Subject: [PATCH 011/117] almost moving to the right place --- .../get-closest-draggable.js | 2 +- src/state/move-to-best-droppable/index.js | 20 ++++++++++++------ src/state/reducer.js | 21 ++++++++++++++----- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index 93b57e4649..7f4aff09d7 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -25,7 +25,7 @@ export default ({ const distanceToB = distance(center, add(b.page.withMargin.center, scrollOffset)); // if a is closer - return a - if (distanceToA > distanceToB) { + if (distanceToA < distanceToB) { return -1; } diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index 4ab2365d67..ebc83b5a0b 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -2,6 +2,7 @@ import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import getClosestDraggable from './get-closest-draggable'; +import { subtract } from '../position'; import type { DraggableId, DroppableId, @@ -26,11 +27,6 @@ type Args = {| droppables: DroppableDimensionMap, |} -type Result = {| - offset: Position, - impact: DragImpact, -|} - export default ({ isMovingForward, center, @@ -38,7 +34,8 @@ export default ({ droppableId, draggables, droppables, - }: Args): ?Result => { + }: Args): ?Position => { + const draggable: DraggableDimension = draggables[draggableId]; const source: DroppableDimension = droppables[droppableId]; const destination: ?DroppableDimension = getBestCrossAxisDroppable({ @@ -89,4 +86,15 @@ export default ({ // also need to force the other draggables to move to needed console.log('is going before?', isGoingBefore); + + // need to line up the top/bottom edge + // need to align to the center position + const newHome: Position = { + x: closestSibling.page.withMargin.center.x, + y: (closestSibling.page.withMargin[isGoingBefore ? 'top' : 'bottom']) + (draggable.page.withMargin.height / 2), + }; + + const offset: Position = subtract(newHome, center); + + return offset; }; diff --git a/src/state/reducer.js b/src/state/reducer.js index e2ab38550a..78028831c3 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -454,11 +454,13 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } - const draggableId: DraggableId = state.drag.current.id; - const center: Position = state.drag.current.page.center; + const current: CurrentDrag = state.drag.current; + + const draggableId: DraggableId = current.id; + const center: Position = current.page.center; const droppableId: DroppableId = state.drag.impact.destination.droppableId; - const result = moveToBestDroppable({ + const offset: ?Position = moveToBestDroppable({ isMovingForward: action.type === 'CROSS_AXIS_MOVE_FORWARD', center, draggableId, @@ -467,9 +469,18 @@ export default (state: State = clean('IDLE'), action: Action): State => { droppables: state.dimension.droppable, }); - console.log('result', result); + if (!offset) { + return state; + } + + const client: Position = add(current.client.selection, offset); + const page: Position = add(current.page.selection, offset); - return state; + return move({ + state, + clientSelection: client, + pageSelection: page, + }); } if (action.type === 'DROP_ANIMATE') { From 9b4e414a384268b3e5f39fe49afc017998f13df3 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 12:45:21 +1000 Subject: [PATCH 012/117] hacking something together --- src/state/get-drag-impact.js | 8 ++-- src/state/get-draggables-inside-droppable.js | 49 ++++++++++++++------ src/state/jump-to-next-index.js | 6 +++ src/state/reducer.js | 1 + stories/src/multiple-vertical/quote-app.js | 2 +- 5 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index bf47986e69..5aeaaf06b0 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -25,7 +25,9 @@ type ImpactArgs = {| page: Position, // used for comparison with other dimensions withinDroppable: WithinDroppable, + // item being dragged draggableId: DraggableId, + // all dimensions in system draggables: DraggableDimensionMap, droppables: DroppableDimensionMap |} @@ -48,14 +50,14 @@ export default ({ const newCenter = withinDroppable.center; const draggingDimension: DraggableDimension = draggables[draggableId]; - const droppableDimension: DroppableDimension = droppables[droppableId]; + const droppable: DroppableDimension = droppables[droppableId]; const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( - droppableDimension, + droppable, draggables, ); - const axis: Axis = droppableDimension.axis; + const axis: Axis = droppable.axis; // not considering margin so that items move based on visible edges const draggableCenter: Position = draggingDimension.page.withoutMargin.center; diff --git a/src/state/get-draggables-inside-droppable.js b/src/state/get-draggables-inside-droppable.js index 857ce8f3b6..91b008ee95 100644 --- a/src/state/get-draggables-inside-droppable.js +++ b/src/state/get-draggables-inside-droppable.js @@ -2,22 +2,43 @@ import memoizeOne from 'memoize-one'; import { draggableMapToList } from './dimension-map-to-list'; import type { + Position, + DraggableId, + DimensionFragment, DraggableDimension, DroppableDimension, DraggableDimensionMap, } from '../types'; -export default memoizeOne( - (droppable: DroppableDimension, - draggables: DraggableDimensionMap, - ): DraggableDimension[] => draggableMapToList(draggables) - .filter((draggable: DraggableDimension): boolean => ( - droppable.id === draggable.droppableId - )) - // Dimensions are not guarenteed to be ordered in the same order as keys - // So we need to sort them so they are in the correct order - .sort((a: DraggableDimension, b: DraggableDimension): number => ( - a.page.withoutMargin.center[droppable.axis.line] - - b.page.withoutMargin.center[droppable.axis.line] - )) -); +const isOverDroppable = (target: Position, droppable: DroppableDimension): boolean => { + const fragment: DimensionFragment = droppable.page.withMargin; + const { top, right, bottom, left } = fragment; + + return target.x >= left && + target.x <= right && + target.y >= top && + target.y <= bottom; +}; + +type Dragging = {| + draggableId: DraggableId, + center: Position, +|} + +export default (droppable: DroppableDimension, + draggables: DraggableDimensionMap, + dragging?: Dragging, +): DraggableDimension[] => draggableMapToList(draggables) + .filter((draggable: DraggableDimension): boolean => { + if (dragging && dragging.draggableId === draggable.id) { + return isOverDroppable(dragging.center, droppable); + } + + return droppable.id === draggable.droppableId; + }) + // Dimensions are not guarenteed to be ordered in the same order as keys + // So we need to sort them so they are in the correct order + .sort((a: DraggableDimension, b: DraggableDimension): number => ( + a.page.withoutMargin.center[droppable.axis.line] - + b.page.withoutMargin.center[droppable.axis.line] + )); diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index 4538227a70..3996e76562 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -23,6 +23,8 @@ const getIndex = memoizeOne( type JumpToNextArgs = {| isMovingForward: boolean, draggableId: DraggableId, + // the current center position + center: Position, impact: DragImpact, draggables: DraggableDimensionMap, droppables: DroppableDimensionMap, @@ -37,6 +39,7 @@ export default ({ isMovingForward, draggableId, impact, + center, draggables, droppables, }: JumpToNextArgs): ?JumpToNextResult => { @@ -54,6 +57,9 @@ export default ({ const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( droppable, draggables, + { draggableId, + center, + } ); const startIndex: number = getIndex(insideDroppable, draggable); diff --git a/src/state/reducer.js b/src/state/reducer.js index 78028831c3..65c24cc5cf 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -402,6 +402,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { isMovingForward, draggableId: existing.current.id, impact: existing.impact, + center: existing.current.page.center, draggables: state.dimension.draggable, droppables: state.dimension.droppable, }); diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js index 1641123922..5957d1cebf 100644 --- a/stories/src/multiple-vertical/quote-app.js +++ b/stories/src/multiple-vertical/quote-app.js @@ -29,7 +29,7 @@ const Column = styled.div` `; const PushDown = styled.div` - height: 400px; + height: 200px; `; const isDraggingClassName = 'is-dragging'; From 53cbf60c4a99d04bbca40ac335ed029e0c56bbe7 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 13:49:42 +1000 Subject: [PATCH 013/117] initial placeholder --- src/state/get-drag-impact.js | 2 + src/state/selectors.js | 43 +++++++++++ src/view/draggable/draggable.jsx | 2 +- src/view/droppable/connected-droppable.js | 75 ++++++++++++++++--- src/view/droppable/droppable-types.js | 10 +++ src/view/droppable/droppable.jsx | 15 ++++ src/view/placeholder/index.js | 1 + .../placeholder.jsx | 0 stories/src/multiple-vertical/quote-list.js | 52 +++++++------ 9 files changed, 168 insertions(+), 32 deletions(-) create mode 100644 src/view/placeholder/index.js rename src/view/{draggable => placeholder}/placeholder.jsx (100%) diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 5aeaaf06b0..9b359deb9d 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -130,6 +130,8 @@ export default ({ isBeyondStartPosition: shouldDisplaceItemsForward, }; + console.log('destination', { droppableId, index }); + const impact: DragImpact = { movement, direction: axis.direction, diff --git a/src/state/selectors.js b/src/state/selectors.js index ef6fc0e51c..892929dc95 100644 --- a/src/state/selectors.js +++ b/src/state/selectors.js @@ -1,7 +1,10 @@ // @flow +import { createSelector } from 'reselect'; import type { PendingDrop, DragState, + DraggableDimension, + DraggableDimensionMap, Phase, State, } from '../types'; @@ -16,3 +19,43 @@ export const pendingDropSelector = (state: State): ?PendingDrop => { }; export const dragSelector = (state: State): ?DragState => state.drag; + +const draggableMapSelector = (state: State): DraggableDimensionMap => state.dimension.draggable; + +export const draggingDraggableSelector = createSelector([ + phaseSelector, + dragSelector, + pendingDropSelector, + draggableMapSelector, +], (phase: Phase, + drag: ?DragState, + pending: ?PendingDrop, + draggables: DraggableDimensionMap + ): ?DraggableDimension => { + if (phase === 'DRAGGING') { + if (!drag) { + console.error('cannot get placeholder dimensions as there is an invalid drag state'); + return null; + } + + const draggable: DraggableDimension = draggables[drag.current.id]; + return draggable; + } + + if (phase === 'DROP_ANIMATING') { + if (!pending) { + console.error('cannot get placeholder dimensions as there is an invalid drag state'); + return null; + } + + if (!pending.result.destination) { + return null; + } + + const draggable: DraggableDimension = draggables[pending.result.destination.id]; + return draggable; + } + + return null; +} +); diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 559a7870f2..24f8a37caf 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -20,7 +20,7 @@ import type { Provided as DragHandleProvided, } from '../drag-handle/drag-handle-types'; import getCenterPosition from '../get-center-position'; -import Placeholder from './placeholder'; +import Placeholder from '../placeholder'; import { droppableIdKey } from '../context-keys'; import { add } from '../../state/position'; import type { diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index 6af5a454a2..d3326815cd 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -3,7 +3,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import memoizeOne from 'memoize-one'; import { storeKey } from '../context-keys'; -import { dragSelector, pendingDropSelector, phaseSelector } from '../../state/selectors'; +import { + dragSelector, + pendingDropSelector, + phaseSelector, + draggingDraggableSelector, +} from '../../state/selectors'; import Droppable from './droppable'; import type { Phase, @@ -12,13 +17,16 @@ import type { State, DroppableId, DraggableLocation, + DraggableDimension, } from '../../types'; import type { OwnProps, MapProps, + Placeholder, } from './droppable-types'; export const makeSelector = () => { + const i = 0; const idSelector = (state: State, ownProps: OwnProps) => ownProps.droppableId; const isDropDisabledSelector = (state: State, ownProps: OwnProps) => @@ -33,19 +41,56 @@ export const makeSelector = () => { }, ); - const getMapProps = memoizeOne((isDraggingOver: boolean): MapProps => ({ - isDraggingOver, - })); + const getPlaceholder = memoizeOne( + (id: DroppableId, + source: DraggableLocation, + destination: ?DraggableLocation, + draggable: ?DraggableDimension + ): ?Placeholder => { + if (!destination) { + return null; + } + // no placeholder needed for this droppable + if (destination.droppableId !== id) { + return null; + } + + // no placeholder needed when dragging over the source list + if (source.droppableId === destination.droppableId) { + return null; + } + + if (!draggable) { + return null; + } + + const placeholder: Placeholder = { + width: draggable.page.withMargin.width, + height: draggable.page.withoutMargin.height, + }; + + return placeholder; + } + ); + + const getMapProps = memoizeOne( + (isDraggingOver: boolean, placeholder: ?Placeholder): MapProps => ({ + isDraggingOver, + placeholder, + }) + ); return createSelector( [phaseSelector, dragSelector, + draggingDraggableSelector, pendingDropSelector, idSelector, isDropDisabledSelector, ], (phase: Phase, drag: ?DragState, + draggable: ?DraggableDimension, pending: ?PendingDrop, id: DroppableId, isDropDisabled: boolean, @@ -57,24 +102,36 @@ export const makeSelector = () => { if (phase === 'DRAGGING') { if (!drag) { console.error('cannot determine dragging over as there is not drag'); - return getMapProps(false); + return getMapProps(false, null); } const isDraggingOver = getIsDraggingOver(id, drag.impact.destination); - return getMapProps(isDraggingOver); + const placeholder: ?Placeholder = getPlaceholder( + id, + drag.initial.source, + drag.impact.destination, + draggable + ); + return getMapProps(isDraggingOver, placeholder); } if (phase === 'DROP_ANIMATING') { if (!pending) { console.error('cannot determine dragging over as there is no pending result'); - return getMapProps(false); + return getMapProps(false, null); } const isDraggingOver = getIsDraggingOver(id, pending.impact.destination); - return getMapProps(isDraggingOver); + const placeholder = getPlaceholder( + id, + pending.result.source, + pending.result.destination, + draggable + ); + return getMapProps(isDraggingOver, placeholder); } - return getMapProps(false); + return getMapProps(false, null); }, ); }; diff --git a/src/view/droppable/droppable-types.js b/src/view/droppable/droppable-types.js index 818f2a3a7f..c1296be59f 100644 --- a/src/view/droppable/droppable-types.js +++ b/src/view/droppable/droppable-types.js @@ -11,8 +11,14 @@ import type { Direction, } from '../../types'; +export type Placeholder = {| + height: number, + width: number, +|} + export type Provided = {| innerRef: (HTMLElement) => void, + placeholder: ?ReactElement, |} export type StateSnapshot = {| @@ -21,6 +27,10 @@ export type StateSnapshot = {| export type MapProps = {| isDraggingOver: boolean, + // placeholder is used to hold space when + // not the user is dragging over a list that + // is not the source list + placeholder: ?Placeholder, |} export type OwnProps = {| diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 10d99413ac..95bf4cbf9e 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import type { Props, Provided, StateSnapshot, DefaultProps } from './droppable-types'; import type { DroppableId, HTMLElement } from '../../types'; import DroppableDimensionPublisher from '../droppable-dimension-publisher/'; +import Placeholder from '../placeholder/'; import { droppableIdKey } from '../context-keys'; type State = {| @@ -60,9 +61,23 @@ export default class Droppable extends Component { }); } + getPlaceholder() { + if (!this.props.placeholder) { + return null; + } + + return ( + + ); + } + render() { const provided: Provided = { innerRef: this.setRef, + placeholder: this.getPlaceholder(), }; const snapshot: StateSnapshot = { isDraggingOver: this.props.isDraggingOver, diff --git a/src/view/placeholder/index.js b/src/view/placeholder/index.js new file mode 100644 index 0000000000..7d723e8f35 --- /dev/null +++ b/src/view/placeholder/index.js @@ -0,0 +1 @@ +export default from './placeholder'; diff --git a/src/view/draggable/placeholder.jsx b/src/view/placeholder/placeholder.jsx similarity index 100% rename from src/view/draggable/placeholder.jsx rename to src/view/placeholder/placeholder.jsx diff --git a/stories/src/multiple-vertical/quote-list.js b/stories/src/multiple-vertical/quote-list.js index b8faca26f2..784e6397f5 100644 --- a/stories/src/multiple-vertical/quote-list.js +++ b/stories/src/multiple-vertical/quote-list.js @@ -14,7 +14,8 @@ import type { StateSnapshot as DraggableStateSnapshot, } from '../../../src/view/draggable/draggable-types'; -const Container = styled.div` +const Wrapper = styled.div` + width: 250px; background-color: ${({ isDraggingOver }) => (isDraggingOver ? colors.blue.lighter : colors.blue.light)}; display: flex; flex-direction: column; @@ -22,7 +23,11 @@ const Container = styled.div` padding-bottom: 0; user-select: none; transition: background-color 0.1s ease; - width: 250px; +`; + +const Container = styled.div` + display: flex; + flex-direction: column; `; const Title = styled.h4` @@ -42,28 +47,31 @@ export default class QuoteList extends Component { return ( {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( - - {listId} - {quotes.map((quote: Quote) => ( - - {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( -
- - {dragProvided.placeholder} -
+ + + {listId} + {quotes.map((quote: Quote) => ( + + {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( +
+ + {dragProvided.placeholder} +
)} -
+
))} -
+ + {dropProvided.placeholder} + )}
); From 6b21ff2332315c8cff0cbf34af84cfd16d0e5b23 Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Mon, 28 Aug 2017 13:58:39 +1000 Subject: [PATCH 014/117] implement mouse dragging between lists, fix tests. --- src/state/get-new-home-client-offset.js | 4 +- .../state/get-new-home-client-offset.spec.js | 267 ++++++++++-------- 2 files changed, 151 insertions(+), 120 deletions(-) diff --git a/src/state/get-new-home-client-offset.js b/src/state/get-new-home-client-offset.js index 15458f6daa..34c5913094 100644 --- a/src/state/get-new-home-client-offset.js +++ b/src/state/get-new-home-client-offset.js @@ -48,13 +48,13 @@ export default ({ } // The dimension of the item being dragged - const draggedDimension: DimensionFragment = draggables[draggableId].client.withoutMargin; + const draggedDimension: DimensionFragment = draggables[draggableId].client.withMargin; // The index of the last item being displaced const displacedIndex: number = isBeyondStartPosition ? movedDraggables.length - 1 : 0; // The dimension of the last item being displaced const displacedDimension: DimensionFragment = draggables[ movedDraggables[displacedIndex] - ].client.withoutMargin; + ].client.withMargin; // Find the difference between the center of the dragging item // and the center of the last item being displaced diff --git a/test/unit/state/get-new-home-client-offset.spec.js b/test/unit/state/get-new-home-client-offset.spec.js index 0bb2c2a67f..5e2353393c 100644 --- a/test/unit/state/get-new-home-client-offset.spec.js +++ b/test/unit/state/get-new-home-client-offset.spec.js @@ -2,7 +2,7 @@ import getNewHomeClientOffset from '../../../src/state/get-new-home-client-offset'; import noImpact from '../../../src/state/no-impact'; import { getDraggableDimension } from '../../../src/state/dimension'; -import { add, subtract } from '../../../src/state/position'; +import { add, negate, subtract } from '../../../src/state/position'; import getClientRect from '../../utils/get-client-rect'; import { vertical, horizontal } from '../../../src/state/axis'; import type { @@ -15,18 +15,26 @@ import type { const origin: Position = { x: 0, y: 0 }; const droppableId: DroppableId = 'drop-1'; +let draggable1: DraggableDimension; +let draggable2: DraggableDimension; +let draggable3: DraggableDimension; +let draggables: DraggableDimensionMap; +let draggableId; + +const getDistanceOverDraggables = dimension => arr => ({ + [dimension === 'height' ? 'y' : 'x']: arr.reduce( + (total, draggable) => total + draggable.page.withMargin[dimension] + 1, + 0 + ), + [dimension === 'height' ? 'x' : 'y']: 0, +}); +const getVerticalDistanceOverDraggables = getDistanceOverDraggables('height'); +const getHorizontalDistanceOverDraggables = getDistanceOverDraggables('width'); describe('get new home client offset', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => { }); - }); - - afterEach(() => { - console.error.mockRestore(); - }); - - describe('vertical', () => { - const draggable1: DraggableDimension = getDraggableDimension({ + draggable1 = getDraggableDimension({ id: 'drag-1', droppableId, clientRect: getClientRect({ @@ -38,7 +46,7 @@ describe('get new home client offset', () => { }); // huge height: 199 - const draggable2: DraggableDimension = getDraggableDimension({ + draggable2 = getDraggableDimension({ id: 'drag-2', droppableId, clientRect: getClientRect({ @@ -50,7 +58,7 @@ describe('get new home client offset', () => { }); // height: 299 - const draggable3: DraggableDimension = getDraggableDimension({ + draggable3 = getDraggableDimension({ id: 'drag-3', droppableId, clientRect: getClientRect({ @@ -61,12 +69,20 @@ describe('get new home client offset', () => { }), }); - const draggables: DraggableDimensionMap = { + draggables = { [draggable1.id]: draggable1, [draggable2.id]: draggable2, [draggable3.id]: draggable3, }; + draggableId = Object.keys(draggables)[0]; + }); + + afterEach(() => { + console.error.mockRestore(); + }); + + describe('vertical', () => { it('should return the total scroll diff if nothing has moved', () => { const offset: Position = { x: 100, @@ -88,13 +104,14 @@ describe('get new home client offset', () => { droppableScrollDiff, windowScrollDiff, draggables, + draggableId, axis: vertical, }); expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); }); - it('should return the total scroll diff is no axis is provided', () => { + it('should return the total scroll diff if no axis is provided', () => { const offset: Position = { x: 100, y: 200, @@ -125,6 +142,7 @@ describe('get new home client offset', () => { droppableScrollDiff, windowScrollDiff, draggables, + draggableId, axis: null, }); @@ -136,11 +154,15 @@ describe('get new home client offset', () => { describe('moving forward', () => { // Moving the first item down into the third position - // Where the users cursor is - const selection: Position = { - x: draggable1.client.withoutMargin.center.x, - y: draggable3.client.withoutMargin.top + 1, - }; + // Where the user's cursor is + let selection: Position; + + beforeEach(() => { + selection = { + x: draggable1.client.withoutMargin.center.x, + y: draggable3.client.withoutMargin.top + 1, + }; + }); // moving the first item down past the third item it('should account for the current client location of the dragging item', () => { @@ -161,10 +183,7 @@ describe('get new home client offset', () => { // How much distance the item needs to travel to be in its new home // from where it started - const verticalChange = { - x: 0, - y: draggable2.page.withMargin.height + draggable3.page.withMargin.height, - }; + const verticalChange = getVerticalDistanceOverDraggables([draggable2, draggable3]); // How far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); // this is the final client offset @@ -177,6 +196,7 @@ describe('get new home client offset', () => { droppableScrollDiff: origin, windowScrollDiff: origin, draggables, + draggableId, axis: vertical, }); @@ -200,10 +220,7 @@ describe('get new home client offset', () => { isBeyondStartPosition: true, }; // this is where it needs to end up - const verticalChange = { - x: 0, - y: draggable2.page.withMargin.height + draggable3.page.withMargin.height, - }; + const verticalChange = getVerticalDistanceOverDraggables([draggable2, draggable3]); // this is how far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); // this is the final client offset @@ -216,6 +233,7 @@ describe('get new home client offset', () => { droppableScrollDiff, windowScrollDiff: origin, draggables, + draggableId, axis: vertical, }); @@ -240,10 +258,7 @@ describe('get new home client offset', () => { isBeyondStartPosition: true, }; // this is where it needs to end up - const verticalChange = { - x: 0, - y: draggable2.page.withMargin.height + draggable3.page.withMargin.height, - }; + const verticalChange = getVerticalDistanceOverDraggables([draggable2, draggable3]); // this is how far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); // this is the final client offset @@ -256,6 +271,7 @@ describe('get new home client offset', () => { droppableScrollDiff: origin, windowScrollDiff, draggables, + draggableId, axis: vertical, }); @@ -266,11 +282,15 @@ describe('get new home client offset', () => { describe('moving backward', () => { // Moving the third item back into the first position - // Where the users cursor is - const selection: Position = { - x: draggable3.client.withoutMargin.center.x, - y: draggable1.client.withoutMargin.bottom - 1, - }; + let selection: Position; + + beforeEach(() => { + draggableId = Object.keys(draggables)[Object.keys(draggables).length - 1]; + selection = { + x: draggable3.client.withoutMargin.center.x, + y: draggable1.client.withoutMargin.bottom - 1, + }; + }); // moving the third item backwards past the first and second item it('should account for the current client location of the dragging item', () => { @@ -291,10 +311,9 @@ describe('get new home client offset', () => { // How much distance the item needs to travel to be in its new home // from where it started - const verticalChange = { - x: 0, - y: -(draggable1.page.withMargin.height + draggable2.page.withMargin.height), - }; + const verticalChange = negate( + getVerticalDistanceOverDraggables([draggable1, draggable2]) + ); // How far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); // this is the final client offset @@ -307,6 +326,7 @@ describe('get new home client offset', () => { droppableScrollDiff: origin, windowScrollDiff: origin, draggables, + draggableId, axis: vertical, }); @@ -329,10 +349,9 @@ describe('get new home client offset', () => { }; // How much distance the item needs to travel to be in its new home // from where it started - const verticalChange = { - x: 0, - y: -(draggable1.page.withMargin.height + draggable2.page.withMargin.height), - }; + const verticalChange = negate( + getVerticalDistanceOverDraggables([draggable1, draggable2]) + ); // How far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); // this is the final client offset @@ -345,6 +364,7 @@ describe('get new home client offset', () => { droppableScrollDiff: origin, windowScrollDiff: origin, draggables, + draggableId, axis: vertical, }); @@ -368,10 +388,9 @@ describe('get new home client offset', () => { isBeyondStartPosition: false, }; // this is where it needs to end up - const verticalChange = { - x: 0, - y: -(draggable1.page.withMargin.height + draggable2.page.withMargin.height), - }; + const verticalChange = negate( + getVerticalDistanceOverDraggables([draggable1, draggable2]) + ); // this is how far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); // this is the final client offset @@ -384,6 +403,7 @@ describe('get new home client offset', () => { droppableScrollDiff, windowScrollDiff: origin, draggables, + draggableId, axis: vertical, }); @@ -393,46 +413,51 @@ describe('get new home client offset', () => { }); describe('horizontal', () => { - const draggable1: DraggableDimension = getDraggableDimension({ - id: 'drag-1', - droppableId, - clientRect: getClientRect({ - top: 0, - left: 0, - bottom: 100, - right: 100, - }), - }); + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { }); + draggable1 = getDraggableDimension({ + id: 'drag-1', + droppableId, + clientRect: getClientRect({ + top: 0, + left: 0, + bottom: 100, + right: 100, + }), + }); - // width: 199 - const draggable2: DraggableDimension = getDraggableDimension({ - id: 'drag-2', - droppableId, - clientRect: getClientRect({ - top: 0, - left: 101, - bottom: 100, - right: 300, - }), - }); + // huge height: 199 + draggable2 = getDraggableDimension({ + id: 'drag-2', + droppableId, + clientRect: getClientRect({ + top: 0, + left: 101, + bottom: 100, + right: 300, + }), + }); - // height: 299 - const draggable3: DraggableDimension = getDraggableDimension({ - id: 'drag-3', - droppableId, - clientRect: getClientRect({ - top: 0, - left: 500, - bottom: 100, - right: 301, - }), - }); + // height: 299 + draggable3 = getDraggableDimension({ + id: 'drag-3', + droppableId, + clientRect: getClientRect({ + top: 0, + left: 301, + bottom: 100, + right: 500, + }), + }); - const draggables: DraggableDimensionMap = { - [draggable1.id]: draggable1, - [draggable2.id]: draggable2, - [draggable3.id]: draggable3, - }; + draggables = { + [draggable1.id]: draggable1, + [draggable2.id]: draggable2, + [draggable3.id]: draggable3, + }; + + draggableId = Object.keys(draggables)[0]; + }); it('should return to the total scroll diff if nothing has moved', () => { const offset: Position = { @@ -455,6 +480,7 @@ describe('get new home client offset', () => { droppableScrollDiff, windowScrollDiff, draggables, + draggableId, axis: horizontal, }); @@ -492,6 +518,7 @@ describe('get new home client offset', () => { droppableScrollDiff, windowScrollDiff, draggables, + draggableId, axis: null, }); @@ -503,11 +530,15 @@ describe('get new home client offset', () => { describe('moving forward', () => { // Moving the first item forward into the third position - // Where the users cursor is - const selection: Position = { - x: draggable3.client.withoutMargin.left + 1, - y: draggable1.client.withoutMargin.center.y, - }; + // Where the user's cursor is + let selection: Position; + + beforeEach(() => { + selection = { + x: draggable3.client.withoutMargin.left + 1, + y: draggable1.client.withoutMargin.center.y, + }; + }); // moving the first item down past the third item it('should account for the current client location of the dragging item', () => { @@ -528,10 +559,7 @@ describe('get new home client offset', () => { // How much distance the item needs to travel to be in its new home // from where it started - const horizontalChange = { - x: draggable2.page.withMargin.width + draggable3.page.withMargin.width, - y: 0, - }; + const horizontalChange = getHorizontalDistanceOverDraggables([draggable2, draggable3]); // How far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); // this is the final client offset @@ -544,6 +572,7 @@ describe('get new home client offset', () => { droppableScrollDiff: origin, windowScrollDiff: origin, draggables, + draggableId, axis: horizontal, }); @@ -567,10 +596,7 @@ describe('get new home client offset', () => { isBeyondStartPosition: true, }; // this is where it needs to end up - const horizontalChange = { - x: draggable2.page.withMargin.width + draggable3.page.withMargin.width, - y: 0, - }; + const horizontalChange = getHorizontalDistanceOverDraggables([draggable2, draggable3]); // this is how far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); // this is the final client offset @@ -583,6 +609,7 @@ describe('get new home client offset', () => { droppableScrollDiff, windowScrollDiff: origin, draggables, + draggableId, axis: horizontal, }); @@ -607,10 +634,7 @@ describe('get new home client offset', () => { isBeyondStartPosition: true, }; // this is where it needs to end up - const horizontalChange = { - x: draggable2.page.withMargin.width + draggable3.page.withMargin.width, - y: 0, - }; + const horizontalChange = getHorizontalDistanceOverDraggables([draggable2, draggable3]); // this is how far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); // this is the final client offset @@ -623,6 +647,7 @@ describe('get new home client offset', () => { droppableScrollDiff: origin, windowScrollDiff, draggables, + draggableId, axis: horizontal, }); @@ -633,11 +658,17 @@ describe('get new home client offset', () => { describe('moving backward', () => { // Moving the third item back into the first position - // Where the users cursor is - const selection: Position = { - x: draggable1.client.withoutMargin.right - 1, - y: draggable3.client.withoutMargin.center.y, - }; + // Where the user's cursor is + let selection: Position; + + beforeEach(() => { + draggableId = Object.keys(draggables)[Object.keys(draggables).length - 1]; + + selection = { + x: draggable1.client.withoutMargin.right - 1, + y: draggable3.client.withoutMargin.center.y, + }; + }); // moving the third item back past the first and second item it('should account for the current client location of the dragging item', () => { @@ -658,10 +689,9 @@ describe('get new home client offset', () => { // How much distance the item needs to travel to be in its new home // from where it started - const horizontalChange = { - x: -(draggable1.page.withMargin.width + draggable2.page.withMargin.width), - y: 0, - }; + const horizontalChange = negate( + getHorizontalDistanceOverDraggables([draggable1, draggable2]) + ); // How far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); // this is the final client offset @@ -674,6 +704,7 @@ describe('get new home client offset', () => { droppableScrollDiff: origin, windowScrollDiff: origin, draggables, + draggableId, axis: horizontal, }); @@ -696,10 +727,9 @@ describe('get new home client offset', () => { }; // How much distance the item needs to travel to be in its new home // from where it started - const horizontalChange = { - x: -(draggable1.page.withMargin.width + draggable2.page.withMargin.width), - y: 0, - }; + const horizontalChange = negate( + getHorizontalDistanceOverDraggables([draggable1, draggable2]) + ); // How far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); // this is the final client offset @@ -712,6 +742,7 @@ describe('get new home client offset', () => { droppableScrollDiff: origin, windowScrollDiff: origin, draggables, + draggableId, axis: horizontal, }); @@ -735,10 +766,9 @@ describe('get new home client offset', () => { isBeyondStartPosition: false, }; // this is where it needs to end up - const horizontalChange = { - x: -(draggable1.page.withMargin.width + draggable2.page.withMargin.width), - y: 0, - }; + const horizontalChange = negate( + getHorizontalDistanceOverDraggables([draggable1, draggable2]) + ); // this is how far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); // this is the final client offset @@ -751,6 +781,7 @@ describe('get new home client offset', () => { droppableScrollDiff, windowScrollDiff: origin, draggables, + draggableId, axis: horizontal, }); From b1dd2659dac2a5560a29667c9343c9e8dea84aa7 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 14:01:11 +1000 Subject: [PATCH 015/117] fixes --- src/state/selectors.js | 2 +- src/view/droppable/connected-droppable.js | 2 +- stories/src/multiple-vertical/quote-list.js | 14 +++++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/state/selectors.js b/src/state/selectors.js index 892929dc95..7d0a36a8c3 100644 --- a/src/state/selectors.js +++ b/src/state/selectors.js @@ -52,7 +52,7 @@ export const draggingDraggableSelector = createSelector([ return null; } - const draggable: DraggableDimension = draggables[pending.result.destination.id]; + const draggable: DraggableDimension = draggables[pending.result.draggableId]; return draggable; } diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index d3326815cd..dbce0644a6 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -66,7 +66,7 @@ export const makeSelector = () => { const placeholder: Placeholder = { width: draggable.page.withMargin.width, - height: draggable.page.withoutMargin.height, + height: draggable.page.withMargin.height, }; return placeholder; diff --git a/stories/src/multiple-vertical/quote-list.js b/stories/src/multiple-vertical/quote-list.js index 784e6397f5..4a28f7fa54 100644 --- a/stories/src/multiple-vertical/quote-list.js +++ b/stories/src/multiple-vertical/quote-list.js @@ -23,9 +23,16 @@ const Wrapper = styled.div` padding-bottom: 0; user-select: none; transition: background-color 0.1s ease; + + /* stop the list collapsing when empty */ + min-height: 150px; `; const Container = styled.div` + /* flex child */ + flex-grow: 1; + + /* flex parent */ display: flex; flex-direction: column; `; @@ -47,11 +54,12 @@ export default class QuoteList extends Component { return ( {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( - + {listId} {quotes.map((quote: Quote) => ( From 1fd2fdbdbfe7988e1555848008ee613ad58aa08f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 14:09:39 +1000 Subject: [PATCH 016/117] adding scroll container --- stories/src/multiple-vertical/quote-app.js | 33 ++++++++++++++-------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js index 5957d1cebf..c14ab4af35 100644 --- a/stories/src/multiple-vertical/quote-app.js +++ b/stories/src/multiple-vertical/quote-app.js @@ -32,6 +32,11 @@ const PushDown = styled.div` height: 200px; `; +const ScrollContainer = styled.div` + overflow: auto; + height: 400px; +`; + const isDraggingClassName = 'is-dragging'; type GroupedQuotes = { @@ -149,20 +154,24 @@ export default class QuoteApp extends Component { style={style} quotes={quotes.beta} /> - + + +
- + + + From 5fcbc7f3c081d2ecf99d9fd0e5cdc9e5f378f3f1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 28 Aug 2017 19:06:31 +1000 Subject: [PATCH 017/117] wip --- src/state/axis.js | 2 + src/state/get-drag-impact.js | 2 - src/state/jump-to-next-index.js | 2 +- src/state/move-to-best-droppable/index.js | 60 ++++++++-------------- src/state/reducer.js | 13 +++-- src/types.js | 2 + stories/src/multiple-vertical/quote-app.js | 48 +++++++---------- 7 files changed, 50 insertions(+), 79 deletions(-) diff --git a/src/state/axis.js b/src/state/axis.js index 1ff8c48c2b..e3bdec7dbb 100644 --- a/src/state/axis.js +++ b/src/state/axis.js @@ -4,6 +4,7 @@ import type { HorizontalAxis, VerticalAxis } from '../types'; export const vertical: VerticalAxis = { direction: 'vertical', line: 'y', + crossLine: 'x', start: 'top', end: 'bottom', size: 'height', @@ -15,6 +16,7 @@ export const vertical: VerticalAxis = { export const horizontal: HorizontalAxis = { direction: 'horizontal', line: 'x', + crossLine: 'y', start: 'left', end: 'right', size: 'width', diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 9b359deb9d..5aeaaf06b0 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -130,8 +130,6 @@ export default ({ isBeyondStartPosition: shouldDisplaceItemsForward, }; - console.log('destination', { droppableId, index }); - const impact: DragImpact = { movement, direction: axis.direction, diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index 3996e76562..90ca22b9e6 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -42,7 +42,7 @@ export default ({ center, draggables, droppables, -}: JumpToNextArgs): ?JumpToNextResult => { + }: JumpToNextArgs): ?JumpToNextResult => { if (!impact.destination) { console.error('cannot move forward when there is not previous destination'); return null; diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index ebc83b5a0b..63ff379bf9 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -2,6 +2,7 @@ import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import getClosestDraggable from './get-closest-draggable'; +import moveToNewSpot from './move-to-new-spot'; import { subtract } from '../position'; import type { DraggableId, @@ -27,6 +28,11 @@ type Args = {| droppables: DroppableDimensionMap, |} +type Result = {| + offset: Position, + impact: DragImpact, +|} + export default ({ isMovingForward, center, @@ -34,7 +40,7 @@ export default ({ droppableId, draggables, droppables, - }: Args): ?Position => { + }: Args): ?Result => { const draggable: DraggableDimension = draggables[draggableId]; const source: DroppableDimension = droppables[droppableId]; @@ -45,11 +51,8 @@ export default ({ droppables, }); - console.log('desintation', destination); - // nothing available to move to if (!destination) { - console.log('no destination found'); return null; } @@ -58,43 +61,20 @@ export default ({ draggables, ); - console.log('new siblings', newSiblings); + const target: ?DraggableDimension = newSiblings.length ? + getClosestDraggable({ + axis: destination.axis, + center, + scrollOffset: destination.scroll.current, + draggables: newSiblings, + }) : null; - if (!newSiblings.length) { - // need to move to the start of the list - console.info('not handled yet!'); - return null; - } - - // Assumption: list must have same width - // All good if smaller - but if bigger then it will be a bit messy - up to consumer - - const closestSibling: DraggableDimension = getClosestDraggable({ - axis: destination.axis, + return moveToNewSpot({ center, - scrollOffset: destination.scroll.current, - draggables: newSiblings, + source, + destination, + draggable, + target, + draggables, }); - - console.log('closest', closestSibling); - - // TODO: what if going from a vertical to horizontal list - - // needs to go before the closest if it is before / equal on the main axis - // Keep in mind that an item 'before' another will have a smaller value on the viewport - const isGoingBefore: boolean = center[source.axis.line] < closestSibling.page.withMargin.center[source.axis.line]; - - // also need to force the other draggables to move to needed - console.log('is going before?', isGoingBefore); - - // need to line up the top/bottom edge - // need to align to the center position - const newHome: Position = { - x: closestSibling.page.withMargin.center.x, - y: (closestSibling.page.withMargin[isGoingBefore ? 'top' : 'bottom']) + (draggable.page.withMargin.height / 2), - }; - - const offset: Position = subtract(newHome, center); - - return offset; }; diff --git a/src/state/reducer.js b/src/state/reducer.js index 65c24cc5cf..d97ee95aef 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -461,7 +461,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { const center: Position = current.page.center; const droppableId: DroppableId = state.drag.impact.destination.droppableId; - const offset: ?Position = moveToBestDroppable({ + const result: ?mixed = moveToBestDroppable({ isMovingForward: action.type === 'CROSS_AXIS_MOVE_FORWARD', center, draggableId, @@ -470,17 +470,16 @@ export default (state: State = clean('IDLE'), action: Action): State => { droppables: state.dimension.droppable, }); - if (!offset) { + if (!result) { return state; } - const client: Position = add(current.client.selection, offset); - const page: Position = add(current.page.selection, offset); - return move({ state, - clientSelection: client, - pageSelection: page, + clientSelection: result.center, + pageSelection: result.center, + impact: result.impact, + shouldAnimate: true, }); } diff --git a/src/types.js b/src/types.js index 0b12d8aa41..b05d4e94fb 100644 --- a/src/types.js +++ b/src/types.js @@ -18,6 +18,7 @@ export type Direction = 'horizontal' | 'vertical'; export type VerticalAxis = {| direction: 'vertical', line: 'y', + crossLine: 'x', start: 'top', end: 'bottom', size: 'height', @@ -29,6 +30,7 @@ export type VerticalAxis = {| export type HorizontalAxis = {| direction: 'horizontal', line: 'x', + crossLine: 'y', start: 'left', end: 'right', size: 'width', diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js index c14ab4af35..4c18ecd82f 100644 --- a/stories/src/multiple-vertical/quote-app.js +++ b/stories/src/multiple-vertical/quote-app.js @@ -25,18 +25,13 @@ const Root = styled.div` `; const Column = styled.div` - + margin: 0 ${grid * 2}px; `; const PushDown = styled.div` height: 200px; `; -const ScrollContainer = styled.div` - overflow: auto; - height: 400px; -`; - const isDraggingClassName = 'is-dragging'; type GroupedQuotes = { @@ -48,7 +43,6 @@ type GroupedQuotes = { type Props = {| initial: GroupedQuotes, - listStyle?: Object, |} type State = {| @@ -127,10 +121,6 @@ export default class QuoteApp extends Component { render() { const { quotes } = this.state; - const style = { - ...this.props.listStyle, - margin: '0 20px', - }; return ( @@ -151,27 +140,28 @@ export default class QuoteApp extends Component { - - - + - - - + From 0bf8c231e4f0feacbbe2818f35f8a31c7a76bbac Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 29 Aug 2017 07:50:21 +1000 Subject: [PATCH 018/117] fixing performance issue when moving to a new droppable --- src/state/get-new-home-client-offset.js | 3 +-- .../move-to-best-droppable/move-to-new-spot.js | 14 ++++++++++++-- src/view/droppable/connected-droppable.js | 12 ++++++++---- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/state/get-new-home-client-offset.js b/src/state/get-new-home-client-offset.js index 34c5913094..c5c456cd93 100644 --- a/src/state/get-new-home-client-offset.js +++ b/src/state/get-new-home-client-offset.js @@ -66,8 +66,7 @@ export default ({ // To account for items of different sizes we need to adjust the offset // between their center points by half their size difference const mainAxisSizeDifference: number = ( - ((draggedDimension[axis.size] - displacedDimension[axis.size]) - / 2) + ((draggedDimension[axis.size] - displacedDimension[axis.size]) / 2) * (isBeyondStartPosition ? -1 : 1) ); const mainAxisSizeOffset: Position = patch(axis.line, mainAxisSizeDifference); diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index 09d476478d..402d999f66 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -2,13 +2,23 @@ import type { DraggableDimension } from '../../types'; // This functions responsibility is to move a draggable above/below a draggable type Args = {| - source: DraggableDimension, - destination: DraggableDimension, |} export default ({ source, destination, }: Args): Result => { + // Moving to an empty droppable + const isGoingBefore: boolean = center[source.axis.line] < closestSibling.page.withMargin.center[source.axis.line]; + if (!target) { + + } + + // Moving to a populated droppable + + // 1. If isGoingBefore: need to move draggable start edge to start edge of target + // Then need to move the target and everything after it forward + // 2. If is going after: need to move draggable start edge to the end of the target + // Then need to move everything after the target forward }; diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index dbce0644a6..0bcc57803d 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -41,6 +41,10 @@ export const makeSelector = () => { }, ); + const memoizedPlaceholder = memoizeOne((width: number, height: number): Placeholder => ({ + width, height, + })); + const getPlaceholder = memoizeOne( (id: DroppableId, source: DraggableLocation, @@ -64,10 +68,10 @@ export const makeSelector = () => { return null; } - const placeholder: Placeholder = { - width: draggable.page.withMargin.width, - height: draggable.page.withMargin.height, - }; + const placeholder: Placeholder = memoizedPlaceholder( + draggable.page.withMargin.width, + draggable.page.withMargin.height, + ); return placeholder; } From e2d2c231b3786a120786237ab4616ad475a9ae7a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 29 Aug 2017 08:19:33 +1000 Subject: [PATCH 019/117] adding explaination and tests for optional patch value --- src/state/position.js | 6 ++++++ test/unit/state/position.spec.js | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/state/position.js b/src/state/position.js index 7adcfc8929..f15c010d30 100644 --- a/src/state/position.js +++ b/src/state/position.js @@ -20,12 +20,18 @@ export const negate = (point: Position): Position => ({ y: point.y !== 0 ? -point.y : 0, }); +// Allows you to build a position from values. +// Really useful when working with the Axis type +// patch('x', 5) = { x: 5, y: 0 } +// patch('x', 5, 1) = { x: 5, y: 1 } export const patch = ( line: 'x' | 'y', value: number, otherValue?: number = 0 ): Position => ({ + // set the value of 'x', or 'y' [line]: value, + // set the value of the other line [line === 'x' ? 'y' : 'x']: otherValue, }); diff --git a/test/unit/state/position.spec.js b/test/unit/state/position.spec.js index f5e8edc6b3..9f9c7ac614 100644 --- a/test/unit/state/position.spec.js +++ b/test/unit/state/position.spec.js @@ -72,6 +72,11 @@ describe('position', () => { it('should patch a position with a x value', () => { expect(patch('y', 5)).toEqual({ x: 0, y: 5 }); }); + + it('should allow patching of the non primary line', () => { + expect(patch('x', 5, 1)).toEqual({ x: 5, y: 1 }); + expect(patch('y', 5, 1)).toEqual({ x: 1, y: 5 }); + }); }); describe('distance', () => { From be3012cf3729a4c165279ff485135984f108e37d Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Tue, 29 Aug 2017 11:03:17 +1000 Subject: [PATCH 020/117] re-write getNewHomeClientOffset and fix tests --- src/state/action-creators.js | 10 +- src/state/get-drag-impact.js | 2 - src/state/get-new-home-client-offset.js | 105 ++++++++++++------ stories/5-multiple-vertical-lists-story.js | 2 +- stories/src/multiple-vertical/quote-app.js | 33 ++---- stories/src/multiple-vertical/quote-list.js | 51 +++++---- .../state/get-new-home-client-offset.spec.js | 65 +++++++---- 7 files changed, 162 insertions(+), 106 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index cad294622b..772f8e0416 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -17,7 +17,7 @@ import type { } from '../types'; import noImpact from './no-impact'; import getNewHomeClientOffset from './get-new-home-client-offset'; -import { add, subtract, isEqual, patch } from './position'; +import { add, subtract, isEqual } from './position'; const origin: Position = { x: 0, y: 0 }; @@ -303,9 +303,9 @@ export const drop = () => }; const scrollDiff = getScrollDiff( - initial, - current, - sourceDroppable, + initial, + current, + sourceDroppable, ); const newHomeOffset: Position = getNewHomeClientOffset({ @@ -315,7 +315,7 @@ export const drop = () => droppableScrollDiff: scrollDiff.droppable, windowScrollDiff: scrollDiff.window, draggables: state.dimension.draggable, - axis: destinationDroppable ? destinationDroppable.axis : null, + destinationDroppable, draggableId: current.id, }); diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 9b359deb9d..5aeaaf06b0 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -130,8 +130,6 @@ export default ({ isBeyondStartPosition: shouldDisplaceItemsForward, }; - console.log('destination', { droppableId, index }); - const impact: DragImpact = { movement, direction: axis.direction, diff --git a/src/state/get-new-home-client-offset.js b/src/state/get-new-home-client-offset.js index 34c5913094..0e67a69931 100644 --- a/src/state/get-new-home-client-offset.js +++ b/src/state/get-new-home-client-offset.js @@ -4,8 +4,8 @@ import type { Position, DimensionFragment, DraggableDimensionMap, + DroppableDimension, DraggableId, - Axis, } from '../types'; import { add, subtract, patch } from './position'; @@ -17,8 +17,7 @@ type NewHomeArgs = {| droppableScrollDiff: Position, windowScrollDiff: Position, draggables: DraggableDimensionMap, - // axis of the destination droppable - axis: ?Axis, + destinationDroppable: ?DroppableDimension, |} type ClientOffset = Position; @@ -32,48 +31,88 @@ export default ({ draggableId, droppableScrollDiff, windowScrollDiff, + destinationDroppable, draggables, - axis, }: NewHomeArgs): ClientOffset => { const { draggables: movedDraggables, isBeyondStartPosition } = movement; + const draggedItem = draggables[draggableId]; + const isWithinHomeDroppable = destinationDroppable && + destinationDroppable.id === draggedItem.droppableId; - // Just animate back to where it started - if (!movedDraggables.length) { + // If there's no destination or if no movement has occurred, return the starting position. + if ( + !destinationDroppable || + (isWithinHomeDroppable && !movedDraggables.length) + ) { return add(droppableScrollDiff, windowScrollDiff); } - if (!axis) { - console.error('should not have any movement if there is no axis'); - return add(droppableScrollDiff, windowScrollDiff); - } + const { + axis, + id: destinationDroppableId, + page: destinationDroppablePage, + } = destinationDroppable; - // The dimension of the item being dragged - const draggedDimension: DimensionFragment = draggables[draggableId].client.withMargin; - // The index of the last item being displaced - const displacedIndex: number = isBeyondStartPosition ? movedDraggables.length - 1 : 0; - // The dimension of the last item being displaced - const displacedDimension: DimensionFragment = draggables[ - movedDraggables[displacedIndex] - ].client.withMargin; - - // Find the difference between the center of the dragging item - // and the center of the last item being displaced - const distanceToDisplacedCenter: Position = subtract( - displacedDimension.center, - draggedDimension.center + // All the draggables in the destination (even the ones that haven't moved) + const draggablesInDestination = Object.keys(draggables).filter( + thisDraggableId => draggables[thisDraggableId].droppableId === destinationDroppableId ); - // To account for items of different sizes we need to adjust the offset - // between their center points by half their size difference - const mainAxisSizeDifference: number = ( - ((draggedDimension[axis.size] - displacedDimension[axis.size]) - / 2) - * (isBeyondStartPosition ? -1 : 1) - ); - const mainAxisSizeOffset: Position = patch(axis.line, mainAxisSizeDifference); + // The dimension of the item being dragged + const draggedDimension: DimensionFragment = draggedItem.client.withMargin; + + // Find the dimension we need to compare the dragged item with + const destinationDimension: DimensionFragment = (() => { + // If we're not dragging into an empty list + if (movedDraggables.length) { + // The index of the last item being displaced + const displacedIndex: number = isBeyondStartPosition ? movedDraggables.length - 1 : 0; + // Return the dimension of the last item being displaced + return draggables[ + movedDraggables[displacedIndex] + ].client.withMargin; + } + + // If we're dragging to the last place in a new droppable + // which has items in it (but which haven't moved) + if (draggablesInDestination.length) { + return draggables[ + draggablesInDestination[draggablesInDestination.length - 1] + ].client.withMargin; + } + + // Otherwise, return the dimension of the empty droppable + return destinationDroppablePage.withMargin; + })(); + + // The main axis edge to compare + const mainAxisDistance: number = (() => { + // If we're moving in after the last draggable in a new droppable + // we match our start edge to its end edge + if ( + !isWithinHomeDroppable && + !movedDraggables.length && + draggablesInDestination.length + ) { + return destinationDimension[axis.end] - draggedDimension[axis.start]; + } + + // If we're moving forwards in our own list we match end edges + if (isBeyondStartPosition) { + return destinationDimension[axis.end] - draggedDimension[axis.end]; + } + + // If we're moving backwards in our own list or into a new list + // we match start edges + return destinationDimension[axis.start] - draggedDimension[axis.start]; + })(); + + // The difference along the cross axis + const crossAxisDistance: number = destinationDimension[axis.crossAxisStart] - + draggedDimension[axis.crossAxisStart]; // Finally, this is how far the dragged item has to travel to be in its new home - const amount: Position = add(distanceToDisplacedCenter, mainAxisSizeOffset); + const amount: Position = patch(axis.line, mainAxisDistance, crossAxisDistance); // How far away it is on the page from where it needs to be const diff: Position = subtract(amount, pageOffset); diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js index 8e11c238e4..dacce84b46 100644 --- a/stories/5-multiple-vertical-lists-story.js +++ b/stories/5-multiple-vertical-lists-story.js @@ -15,7 +15,7 @@ const initialQuotes = { alpha: namespaceQuoteIds(getQuotes(10), 'alpha'), beta: namespaceQuoteIds(getQuotes(3), 'beta'), gamma: namespaceQuoteIds(getQuotes(10), 'gamma'), - delta: namespaceQuoteIds(getQuotes(5), 'delta'), + delta: namespaceQuoteIds(getQuotes(0), 'delta'), }; storiesOf('multiple vertical lists', module) diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js index c14ab4af35..5957d1cebf 100644 --- a/stories/src/multiple-vertical/quote-app.js +++ b/stories/src/multiple-vertical/quote-app.js @@ -32,11 +32,6 @@ const PushDown = styled.div` height: 200px; `; -const ScrollContainer = styled.div` - overflow: auto; - height: 400px; -`; - const isDraggingClassName = 'is-dragging'; type GroupedQuotes = { @@ -154,24 +149,20 @@ export default class QuoteApp extends Component { style={style} quotes={quotes.beta} /> - - - + - - - + diff --git a/stories/src/multiple-vertical/quote-list.js b/stories/src/multiple-vertical/quote-list.js index 4a28f7fa54..0be5169e9d 100644 --- a/stories/src/multiple-vertical/quote-list.js +++ b/stories/src/multiple-vertical/quote-list.js @@ -23,11 +23,18 @@ const Wrapper = styled.div` padding-bottom: 0; user-select: none; transition: background-color 0.1s ease; +`; +const DropZone = styled.div` /* stop the list collapsing when empty */ min-height: 150px; `; +const ScrollContainer = styled.div` + overflow: auto; + max-height: 400px; +`; + const Container = styled.div` /* flex child */ flex-grow: 1; @@ -58,27 +65,29 @@ export default class QuoteList extends Component { style={style} isDraggingOver={dropSnapshot.isDraggingOver} > - - {listId} - {quotes.map((quote: Quote) => ( - - {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( -
- - {dragProvided.placeholder} -
- )} -
- ))} -
- {dropProvided.placeholder} + + + {listId} + + {quotes.map((quote: Quote) => ( + + {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( +
+ + {dragProvided.placeholder} +
+ )} +
+ ))} + {dropProvided.placeholder} +
+
+
)} diff --git a/test/unit/state/get-new-home-client-offset.spec.js b/test/unit/state/get-new-home-client-offset.spec.js index 5e2353393c..edccf86cb4 100644 --- a/test/unit/state/get-new-home-client-offset.spec.js +++ b/test/unit/state/get-new-home-client-offset.spec.js @@ -1,7 +1,7 @@ // @flow import getNewHomeClientOffset from '../../../src/state/get-new-home-client-offset'; import noImpact from '../../../src/state/no-impact'; -import { getDraggableDimension } from '../../../src/state/dimension'; +import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; import { add, negate, subtract } from '../../../src/state/position'; import getClientRect from '../../utils/get-client-rect'; import { vertical, horizontal } from '../../../src/state/axis'; @@ -11,6 +11,7 @@ import type { Position, DraggableDimension, DraggableDimensionMap, + DroppableDimension, } from '../../../src/types'; const origin: Position = { x: 0, y: 0 }; @@ -20,6 +21,7 @@ let draggable2: DraggableDimension; let draggable3: DraggableDimension; let draggables: DraggableDimensionMap; let draggableId; +let destinationDroppable: DroppableDimension; const getDistanceOverDraggables = dimension => arr => ({ [dimension === 'height' ? 'y' : 'x']: arr.reduce( @@ -76,6 +78,16 @@ describe('get new home client offset', () => { }; draggableId = Object.keys(draggables)[0]; + + destinationDroppable = getDroppableDimension({ + id: droppableId, + clientRect: getClientRect({ + top: 0, + left: 0, + bottom: 600, + right: 100, + }), + }); }); afterEach(() => { @@ -105,13 +117,13 @@ describe('get new home client offset', () => { windowScrollDiff, draggables, draggableId, - axis: vertical, + destinationDroppable, }); expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); }); - it('should return the total scroll diff if no axis is provided', () => { + it('should return the total scroll diff if no destination is provided', () => { const offset: Position = { x: 100, y: 200, @@ -143,12 +155,10 @@ describe('get new home client offset', () => { windowScrollDiff, draggables, draggableId, - axis: null, + destinationDroppable: null, }); expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); - // this is an error situation - expect(console.error).toHaveBeenCalled(); }); describe('moving forward', () => { @@ -197,7 +207,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: vertical, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -234,7 +244,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: vertical, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -272,7 +282,7 @@ describe('get new home client offset', () => { windowScrollDiff, draggables, draggableId, - axis: vertical, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -327,7 +337,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: vertical, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -365,7 +375,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: vertical, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -404,7 +414,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: vertical, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -457,6 +467,17 @@ describe('get new home client offset', () => { }; draggableId = Object.keys(draggables)[0]; + + destinationDroppable = getDroppableDimension({ + id: droppableId, + direction: 'horizontal', + clientRect: getClientRect({ + top: 0, + left: 0, + bottom: 100, + right: 500, + }), + }); }); it('should return to the total scroll diff if nothing has moved', () => { @@ -481,13 +502,13 @@ describe('get new home client offset', () => { windowScrollDiff, draggables, draggableId, - axis: horizontal, + destinationDroppable, }); expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); }); - it('should return the total scroll diff is no axis is provided', () => { + it('should return the total scroll diff is no destination is provided', () => { const offset: Position = { x: 100, y: 200, @@ -519,12 +540,10 @@ describe('get new home client offset', () => { windowScrollDiff, draggables, draggableId, - axis: null, + destinationDroppable: null, }); expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); - // this is an error situation - expect(console.error).toHaveBeenCalled(); }); describe('moving forward', () => { @@ -573,7 +592,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: horizontal, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -610,7 +629,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: horizontal, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -648,7 +667,7 @@ describe('get new home client offset', () => { windowScrollDiff, draggables, draggableId, - axis: horizontal, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -705,7 +724,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: horizontal, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -743,7 +762,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: horizontal, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); @@ -782,7 +801,7 @@ describe('get new home client offset', () => { windowScrollDiff: origin, draggables, draggableId, - axis: horizontal, + destinationDroppable, }); expect(newHomeOffset).toEqual(expected); From 1f00de00012616982cb951a5a9684fce2760bdad Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 29 Aug 2017 11:24:04 +1000 Subject: [PATCH 021/117] wip --- .../move-to-new-spot.js | 74 +++++++++++++++++-- src/state/move-to-edge.js | 32 ++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/state/move-to-edge.js diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index 402d999f66..ea99b82041 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -1,24 +1,88 @@ // @flow -import type { DraggableDimension } from '../../types'; -// This functions responsibility is to move a draggable above/below a draggable +import { subtract, patch } from '../position'; +import type { + Axis, + Position, + DragImpact, + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, +} from '../../types'; + type Args = {| + // the center position of the current draggable + center: Position, + // the draggable that is dragging and needs to move + draggable: DraggableDimension, + // what the draggable is moving towards + // can be null if the destination is empty + target: ?DraggableDimension, + // the droppable the draggable is currently in + source: DroppableDimension, + // the droppable the draggable is moving to + destination: DroppableDimension, + // all the draggables in the system + draggables: DraggableDimensionMap, +|} + +type Result = {| + // how far the draggable needs to move to be in its new home + diff: Position, + // The impact of the movement + impact: DragImpact, |} export default ({ + center, source, destination, + draggable, + target, + draggables, }: Args): Result => { - // Moving to an empty droppable - const isGoingBefore: boolean = center[source.axis.line] < closestSibling.page.withMargin.center[source.axis.line]; + const destinationAxis: Axis = destination.axis; + const sourceAxis: Axis = source.axis; + // 1. Moving to an empty droppable if (!target) { + // Move to start edge of the destination + // based on the axis of the destination + + // start edge of draggable needs to line up + // with start edge of destination + const newHome: Position = { + }; + + const diff: Position = subtract(center, newHome); + + const impact: DragImpact = { + movement: { + draggables: [], + amount: patch(destinationAxis.line, draggable.page.withMargin[destinationAxis.size]), + // TODO: not sure what this should be + isBeyondStartPosition: false, + }, + direction: destinationAxis.direction, + destination: { + droppableId: destination.id, + index: 0, + }, + }; + + return { + diff, impact, + }; } - // Moving to a populated droppable + // 2. Moving to a populated droppable // 1. If isGoingBefore: need to move draggable start edge to start edge of target // Then need to move the target and everything after it forward // 2. If is going after: need to move draggable start edge to the end of the target // Then need to move everything after the target forward + const isGoingBefore: boolean = center[sourceAxis.line] < + target.page.withMargin.center[sourceAxis.line]; + + const newHome = }; diff --git a/src/state/move-to-edge.js b/src/state/move-to-edge.js new file mode 100644 index 0000000000..504d83df18 --- /dev/null +++ b/src/state/move-to-edge.js @@ -0,0 +1,32 @@ +// @flow +import type { + Axis, + Position, + DraggableDimension, + DimensionFragment, +} from '../types'; + +type Edge = 'start' | 'end'; + +type Args = {| + source: DraggableDimension, + sourceEdge: Edge, + destination: DimensionFragment, + destinationEdge: Edge, + destinationAxis: Axis, +|} + +// being clear that this function returns the new center position +type CenterPosition = Position; + +export default ({ + source, + sourceEdge, + destination, + destinationEdge, + destinationAxis, +}: Args): CenterPosition => { + // Wanting to move the edge of a draggable to the edge of something else + + +}; From b37f77be024efde43c85fe1a3d16fee53a90123c Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Tue, 29 Aug 2017 14:15:50 +1000 Subject: [PATCH 022/117] fix drag impact when dragging between multiple lists with internal scrolling --- src/state/action-creators.js | 2 +- src/state/get-drag-impact.js | 25 ++++++++++++++++++++- src/state/get-new-home-client-offset.js | 2 +- stories/src/multiple-vertical/quote-app.js | 1 + stories/src/multiple-vertical/quote-list.js | 5 +++-- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 772f8e0416..3bcbb26324 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -305,7 +305,7 @@ export const drop = () => const scrollDiff = getScrollDiff( initial, current, - sourceDroppable, + destinationDroppable || sourceDroppable, ); const newHomeOffset: Position = getNewHomeClientOffset({ diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 5aeaaf06b0..f829dbad4f 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -17,6 +17,24 @@ import getDroppableOver from './get-droppable-over'; import getDraggablesInsideDroppable from './get-draggables-inside-droppable'; import noImpact from './no-impact'; +// Calculates the net scroll diff along the main axis +// between two droppables with internal scrolling +const getDroppablesScrollDiff = ({ + sourceDroppable, + destinationDroppable, + line, +}: { + sourceDroppable: DroppableDimension, + destinationDroppable: DroppableDimension, + line: 'x' | 'y', +}): number => { + const sourceScrollDiff = sourceDroppable.scroll.initial[line] - + sourceDroppable.scroll.current[line]; + const destinationScrollDiff = destinationDroppable.scroll.initial[line] - + destinationDroppable.scroll.current[line]; + return destinationScrollDiff - sourceScrollDiff; +}; + // It is the responsibility of this function // to return the impact of a drag @@ -77,7 +95,12 @@ export default ({ // If we're over a new droppable items will be displaced // if they sit ahead of the dragging item if (!isWithinHomeDroppable) { - return newCenter[axis.line] < fragment[axis.end]; + const scrollDiff = getDroppablesScrollDiff({ + sourceDroppable: droppables[draggingDimension.droppableId], + destinationDroppable: droppable, + line: axis.line, + }); + return (newCenter[axis.line] - scrollDiff) < fragment[axis.end]; } if (isBeyondStartPosition) { diff --git a/src/state/get-new-home-client-offset.js b/src/state/get-new-home-client-offset.js index 0e67a69931..1bb245ecf6 100644 --- a/src/state/get-new-home-client-offset.js +++ b/src/state/get-new-home-client-offset.js @@ -7,7 +7,7 @@ import type { DroppableDimension, DraggableId, } from '../types'; -import { add, subtract, patch } from './position'; +import { add, patch, subtract } from './position'; type NewHomeArgs = {| movement: DragMovement, diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.js index d6ece527da..4acad5d1c6 100644 --- a/stories/src/multiple-vertical/quote-app.js +++ b/stories/src/multiple-vertical/quote-app.js @@ -132,6 +132,7 @@ export default class QuoteApp extends Component { diff --git a/stories/src/multiple-vertical/quote-list.js b/stories/src/multiple-vertical/quote-list.js index 61dd5041a2..54a846657c 100644 --- a/stories/src/multiple-vertical/quote-list.js +++ b/stories/src/multiple-vertical/quote-list.js @@ -31,7 +31,8 @@ const DropZone = styled.div` `; const ScrollContainer = styled.div` - overflow: auto; + overflow-x: hidden; + overflow-y: auto; max-height: 400px; `; @@ -57,7 +58,7 @@ export default class QuoteList extends Component { internalScroll?: boolean, |} - renderBoard = (dropProvided) => { + renderBoard = (dropProvided: DroppableProvided) => { const { listId, listType, quotes } = this.props; return ( From fe2dc652b1cdf03d148ae66da2a1581640592983 Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Tue, 29 Aug 2017 14:31:41 +1000 Subject: [PATCH 023/117] fix minor type error in action-creators --- src/state/action-creators.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 3bcbb26324..f8419e91ae 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -195,7 +195,7 @@ export type CrossAxisMoveForwardAction = {| payload: DraggableId |} -export const crossAxisMoveForward = (id: DraggableId): MoveForwardAction => ({ +export const crossAxisMoveForward = (id: DraggableId): CrossAxisMoveForwardAction => ({ type: 'CROSS_AXIS_MOVE_FORWARD', payload: id, }); @@ -205,7 +205,7 @@ export type CrossAxisMoveBackwardAction = {| payload: DraggableId |} -export const crossAxisMoveBackward = (id: DraggableId): MoveForwardAction => ({ +export const crossAxisMoveBackward = (id: DraggableId): CrossAxisMoveBackwardAction => ({ type: 'CROSS_AXIS_MOVE_BACKWARD', payload: id, }); From fa47b6458da8d3888fd18e783cac48f5d7058b1a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 29 Aug 2017 14:34:09 +1000 Subject: [PATCH 024/117] move-to-edge --- src/state/move-to-best-droppable/index.js | 8 +-- .../move-to-new-spot.js | 57 +++++++++++++++---- src/state/move-to-edge.js | 33 ++++++++++- src/state/position.js | 7 ++- src/state/reducer.js | 3 +- .../{quote-app.js => quote-app.jsx} | 0 .../{quote-list.js => quote-list.jsx} | 2 +- 7 files changed, 87 insertions(+), 23 deletions(-) rename stories/src/multiple-vertical/{quote-app.js => quote-app.jsx} (100%) rename stories/src/multiple-vertical/{quote-list.js => quote-list.jsx} (99%) diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index 63ff379bf9..085d2710a8 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -3,12 +3,11 @@ import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import getClosestDraggable from './get-closest-draggable'; import moveToNewSpot from './move-to-new-spot'; -import { subtract } from '../position'; +import type { Result } from './move-to-new-spot'; import type { DraggableId, DroppableId, Position, - DragImpact, DroppableDimension, DraggableDimension, DraggableDimensionMap, @@ -28,11 +27,6 @@ type Args = {| droppables: DroppableDimensionMap, |} -type Result = {| - offset: Position, - impact: DragImpact, -|} - export default ({ isMovingForward, center, diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index ea99b82041..e66a0da809 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -1,5 +1,6 @@ // @flow import { subtract, patch } from '../position'; +import moveToEdge from '../move-to-edge'; import type { Axis, Position, @@ -10,7 +11,7 @@ import type { } from '../../types'; type Args = {| - // the center position of the current draggable + // the current center position of the draggable center: Position, // the draggable that is dragging and needs to move draggable: DraggableDimension, @@ -25,9 +26,9 @@ type Args = {| draggables: DraggableDimensionMap, |} -type Result = {| +export type Result = {| // how far the draggable needs to move to be in its new home - diff: Position, + center: Position, // The impact of the movement impact: DragImpact, |} @@ -50,11 +51,13 @@ export default ({ // start edge of draggable needs to line up // with start edge of destination - const newHome: Position = { - - }; - - const diff: Position = subtract(center, newHome); + const newCenter: Position = moveToEdge({ + source: draggable, + sourceEdge: 'start', + destination: destination.page.withMargin, + destinationEdge: 'start', + destinationAxis, + }); const impact: DragImpact = { movement: { @@ -71,7 +74,8 @@ export default ({ }; return { - diff, impact, + center: newCenter, + impact, }; } @@ -81,8 +85,39 @@ export default ({ // Then need to move the target and everything after it forward // 2. If is going after: need to move draggable start edge to the end of the target // Then need to move everything after the target forward - const isGoingBefore: boolean = center[sourceAxis.line] < + const isGoingBeforeTarget: boolean = center[sourceAxis.line] < target.page.withMargin.center[sourceAxis.line]; - const newHome = + console.log(`moving ${draggable.id}`); + console.log('source edge:', isGoingBeforeTarget ? 'end' : 'start'); + console.log('destination edge:', isGoingBeforeTarget ? 'start' : 'end'); + + const newCenter: Position = moveToEdge({ + source: draggable, + // TODO: source edge will always be start - unless moving to home column? + sourceEdge: 'start', + destination: target.page.withMargin, + destinationEdge: isGoingBeforeTarget ? 'start' : 'end', + destinationAxis, + }); + + // TODO + const impact: DragImpact = { + movement: { + draggables: [], + amount: { x: 0, y: 0 }, + // TODO: not sure what this should be + isBeyondStartPosition: false, + }, + direction: destinationAxis.direction, + destination: { + droppableId: destination.id, + index: 0, + }, + }; + + return { + center: newCenter, + impact, + }; }; diff --git a/src/state/move-to-edge.js b/src/state/move-to-edge.js index 504d83df18..c733644f00 100644 --- a/src/state/move-to-edge.js +++ b/src/state/move-to-edge.js @@ -1,4 +1,5 @@ // @flow +import { absolute, add, patch, negate, subtract } from './position'; import type { Axis, Position, @@ -16,9 +17,16 @@ type Args = {| destinationAxis: Axis, |} -// being clear that this function returns the new center position +// Being clear that this function returns the new center position type CenterPosition = Position; +// This function will return the center position required to move +// a draggable to the edge of a dimension fragment (could be a droppable or draggable). +// The center position will be aligned to the axis.crossAxisStart value rather than +// the center position of the destination. This allows for generally a better +// experience when moving between lists of different cross axis size. +// The size difference can be caused by the presence or absence of scroll bars + export default ({ source, sourceEdge, @@ -26,7 +34,28 @@ export default ({ destinationEdge, destinationAxis, }: Args): CenterPosition => { - // Wanting to move the edge of a draggable to the edge of something else + const getCorner = (fragment: DimensionFragment): Position => patch( + destinationAxis.line, + fragment[destinationAxis[destinationEdge]], + fragment[destinationAxis.crossAxisStart] + ); + + // 1. Find the intersection corner point + // 2. add the difference between that point and the center of the dimension + + const corner: Position = getCorner(destination); + + // the difference between the center of the draggable and its corner + const centerDiff = absolute(subtract( + source.page.withoutMargin.center, + getCorner(source.page.withoutMargin) + )); + const signed: Position = patch( + destinationAxis.line, + (sourceEdge === 'start' ? 1 : -1) * centerDiff[destinationAxis.line], + centerDiff[destinationAxis.crossLine], + ); + return add(corner, signed); }; diff --git a/src/state/position.js b/src/state/position.js index f15c010d30..0b28eaf86e 100644 --- a/src/state/position.js +++ b/src/state/position.js @@ -20,10 +20,15 @@ export const negate = (point: Position): Position => ({ y: point.y !== 0 ? -point.y : 0, }); +export const absolute = (point: Position): Position => ({ + x: Math.abs(point.x), + y: Math.abs(point.y), +}); + // Allows you to build a position from values. // Really useful when working with the Axis type // patch('x', 5) = { x: 5, y: 0 } -// patch('x', 5, 1) = { x: 5, y: 1 } +// patch('y', 5, 1) = { x: 1, y: 5 } export const patch = ( line: 'x' | 'y', value: number, diff --git a/src/state/reducer.js b/src/state/reducer.js index d97ee95aef..6c11d4fb26 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -24,6 +24,7 @@ import { add, subtract, negate } from './position'; import getDragImpact from './get-drag-impact'; import jumpToNextIndex from './jump-to-next-index'; import type { JumpToNextResult } from './jump-to-next-index'; +import type { Result as MoveToNewSpotResult } from './move-to-best-droppable/move-to-new-spot'; import getDroppableOver from './get-droppable-over'; import moveToBestDroppable from './move-to-best-droppable/'; @@ -461,7 +462,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { const center: Position = current.page.center; const droppableId: DroppableId = state.drag.impact.destination.droppableId; - const result: ?mixed = moveToBestDroppable({ + const result: ?MoveToNewSpotResult = moveToBestDroppable({ isMovingForward: action.type === 'CROSS_AXIS_MOVE_FORWARD', center, draggableId, diff --git a/stories/src/multiple-vertical/quote-app.js b/stories/src/multiple-vertical/quote-app.jsx similarity index 100% rename from stories/src/multiple-vertical/quote-app.js rename to stories/src/multiple-vertical/quote-app.jsx diff --git a/stories/src/multiple-vertical/quote-list.js b/stories/src/multiple-vertical/quote-list.jsx similarity index 99% rename from stories/src/multiple-vertical/quote-list.js rename to stories/src/multiple-vertical/quote-list.jsx index 61dd5041a2..c0aa15b98d 100644 --- a/stories/src/multiple-vertical/quote-list.js +++ b/stories/src/multiple-vertical/quote-list.jsx @@ -27,7 +27,7 @@ const Wrapper = styled.div` const DropZone = styled.div` /* stop the list collapsing when empty */ - min-height: 150px; + min-height: 250px; `; const ScrollContainer = styled.div` From e91cbfe526470596b92f45a97db4eb80aec070cb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 29 Aug 2017 14:47:27 +1000 Subject: [PATCH 025/117] making source a fragment --- src/state/move-to-best-droppable/move-to-new-spot.js | 12 ++++++++---- src/state/move-to-edge.js | 9 ++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index e66a0da809..543dfb00a9 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -43,6 +43,10 @@ export default ({ }: Args): Result => { const destinationAxis: Axis = destination.axis; const sourceAxis: Axis = source.axis; + const amount: Position = patch( + destinationAxis.line, + draggable.page.withMargin[destinationAxis.size] + ); // 1. Moving to an empty droppable if (!target) { @@ -52,7 +56,7 @@ export default ({ // start edge of draggable needs to line up // with start edge of destination const newCenter: Position = moveToEdge({ - source: draggable, + source: draggable.page.withMargin, sourceEdge: 'start', destination: destination.page.withMargin, destinationEdge: 'start', @@ -62,7 +66,7 @@ export default ({ const impact: DragImpact = { movement: { draggables: [], - amount: patch(destinationAxis.line, draggable.page.withMargin[destinationAxis.size]), + amount, // TODO: not sure what this should be isBeyondStartPosition: false, }, @@ -93,7 +97,7 @@ export default ({ console.log('destination edge:', isGoingBeforeTarget ? 'start' : 'end'); const newCenter: Position = moveToEdge({ - source: draggable, + source: draggable.page.withMargin, // TODO: source edge will always be start - unless moving to home column? sourceEdge: 'start', destination: target.page.withMargin, @@ -105,7 +109,7 @@ export default ({ const impact: DragImpact = { movement: { draggables: [], - amount: { x: 0, y: 0 }, + amount, // TODO: not sure what this should be isBeyondStartPosition: false, }, diff --git a/src/state/move-to-edge.js b/src/state/move-to-edge.js index c733644f00..0032c73d24 100644 --- a/src/state/move-to-edge.js +++ b/src/state/move-to-edge.js @@ -1,16 +1,15 @@ // @flow -import { absolute, add, patch, negate, subtract } from './position'; +import { absolute, add, patch, subtract } from './position'; import type { Axis, Position, - DraggableDimension, DimensionFragment, } from '../types'; type Edge = 'start' | 'end'; type Args = {| - source: DraggableDimension, + source: DimensionFragment, sourceEdge: Edge, destination: DimensionFragment, destinationEdge: Edge, @@ -47,8 +46,8 @@ export default ({ // the difference between the center of the draggable and its corner const centerDiff = absolute(subtract( - source.page.withoutMargin.center, - getCorner(source.page.withoutMargin) + source.center, + getCorner(source) )); const signed: Position = patch( From ca9568d3241e01a58ca390ff456f87ad3c7a3025 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 29 Aug 2017 15:04:26 +1000 Subject: [PATCH 026/117] better keyboard transitions --- src/state/move-to-best-droppable/move-to-new-spot.js | 2 +- src/state/move-to-edge.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index 543dfb00a9..f45f193285 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -97,7 +97,7 @@ export default ({ console.log('destination edge:', isGoingBeforeTarget ? 'start' : 'end'); const newCenter: Position = moveToEdge({ - source: draggable.page.withMargin, + source: draggable.page.withoutMargin, // TODO: source edge will always be start - unless moving to home column? sourceEdge: 'start', destination: target.page.withMargin, diff --git a/src/state/move-to-edge.js b/src/state/move-to-edge.js index 0032c73d24..3329aa91f1 100644 --- a/src/state/move-to-edge.js +++ b/src/state/move-to-edge.js @@ -52,7 +52,8 @@ export default ({ const signed: Position = patch( destinationAxis.line, - (sourceEdge === 'start' ? 1 : -1) * centerDiff[destinationAxis.line], + // if moving to the sourceEdge then we need to pull backwards on the main axis + (sourceEdge === 'end' ? -1 : 1) * centerDiff[destinationAxis.line], centerDiff[destinationAxis.crossLine], ); From c8bff8b5af224a8536e45999e0a9c4de811d4f09 Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Tue, 29 Aug 2017 16:41:22 +1000 Subject: [PATCH 027/117] getNewHomeClientOffset works for all multiple vertical cases. tidy function and add types --- src/state/dimension-map-to-list.js | 3 +- src/state/get-new-home-client-offset.js | 81 +++++++++++---------- stories/src/multiple-vertical/quote-app.jsx | 1 - 3 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/state/dimension-map-to-list.js b/src/state/dimension-map-to-list.js index 86e13027a6..a556db67f7 100644 --- a/src/state/dimension-map-to-list.js +++ b/src/state/dimension-map-to-list.js @@ -15,5 +15,4 @@ export const droppableMapToList = memoizeOne( export const draggableMapToList = memoizeOne( (draggables: DraggableDimensionMap) => Object.keys(draggables).map((id: DraggableId) => draggables[id]) -) -; +); diff --git a/src/state/get-new-home-client-offset.js b/src/state/get-new-home-client-offset.js index 1bb245ecf6..48c5c241bc 100644 --- a/src/state/get-new-home-client-offset.js +++ b/src/state/get-new-home-client-offset.js @@ -1,13 +1,16 @@ // @flow import type { - DragMovement, - Position, DimensionFragment, + DraggableDimension, DraggableDimensionMap, - DroppableDimension, DraggableId, + DragMovement, + DroppableDimension, + Position, } from '../types'; -import { add, patch, subtract } from './position'; +import { add, subtract } from './position'; +import moveToEdge from './move-to-edge'; +import { draggableMapToList } from './dimension-map-to-list'; type NewHomeArgs = {| movement: DragMovement, @@ -18,9 +21,7 @@ type NewHomeArgs = {| windowScrollDiff: Position, draggables: DraggableDimensionMap, destinationDroppable: ?DroppableDimension, -|} - -type ClientOffset = Position; +|}; // Returns the client offset required to move an item from its // original client position to its final resting position @@ -33,11 +34,13 @@ export default ({ windowScrollDiff, destinationDroppable, draggables, -}: NewHomeArgs): ClientOffset => { +}: NewHomeArgs): Position => { const { draggables: movedDraggables, isBeyondStartPosition } = movement; - const draggedItem = draggables[draggableId]; - const isWithinHomeDroppable = destinationDroppable && - destinationDroppable.id === draggedItem.droppableId; + const draggedItem: DraggableDimension = draggables[draggableId]; + const isWithinHomeDroppable: boolean = Boolean( + destinationDroppable && + destinationDroppable.id === draggedItem.droppableId + ); // If there's no destination or if no movement has occurred, return the starting position. if ( @@ -54,15 +57,15 @@ export default ({ } = destinationDroppable; // All the draggables in the destination (even the ones that haven't moved) - const draggablesInDestination = Object.keys(draggables).filter( - thisDraggableId => draggables[thisDraggableId].droppableId === destinationDroppableId + const draggablesInDestination: DraggableDimension[] = draggableMapToList(draggables).filter( + draggable => draggable.droppableId === destinationDroppableId ); // The dimension of the item being dragged - const draggedDimension: DimensionFragment = draggedItem.client.withMargin; + const draggedDimensionFragment: DimensionFragment = draggedItem.client.withMargin; // Find the dimension we need to compare the dragged item with - const destinationDimension: DimensionFragment = (() => { + const destinationDimensionFragment: DimensionFragment = (() => { // If we're not dragging into an empty list if (movedDraggables.length) { // The index of the last item being displaced @@ -76,8 +79,8 @@ export default ({ // If we're dragging to the last place in a new droppable // which has items in it (but which haven't moved) if (draggablesInDestination.length) { - return draggables[ - draggablesInDestination[draggablesInDestination.length - 1] + return draggablesInDestination[ + draggablesInDestination.length - 1 ].client.withMargin; } @@ -85,43 +88,43 @@ export default ({ return destinationDroppablePage.withMargin; })(); - // The main axis edge to compare - const mainAxisDistance: number = (() => { + const { sourceEdge, destinationEdge } = (() => { // If we're moving in after the last draggable in a new droppable // we match our start edge to its end edge - if ( - !isWithinHomeDroppable && + if (!isWithinHomeDroppable && !movedDraggables.length && - draggablesInDestination.length - ) { - return destinationDimension[axis.end] - draggedDimension[axis.start]; + draggablesInDestination.length) { + return { sourceEdge: 'start', destinationEdge: 'end' }; } // If we're moving forwards in our own list we match end edges if (isBeyondStartPosition) { - return destinationDimension[axis.end] - draggedDimension[axis.end]; + return { sourceEdge: 'end', destinationEdge: 'end' }; } // If we're moving backwards in our own list or into a new list // we match start edges - return destinationDimension[axis.start] - draggedDimension[axis.start]; + return { sourceEdge: 'start', destinationEdge: 'start' }; })(); - // The difference along the cross axis - const crossAxisDistance: number = destinationDimension[axis.crossAxisStart] - - draggedDimension[axis.crossAxisStart]; + // This is the draggable's new home + const destination: Position = moveToEdge({ + source: draggedDimensionFragment, + sourceEdge, + destination: destinationDimensionFragment, + destinationEdge, + destinationAxis: axis, + }); - // Finally, this is how far the dragged item has to travel to be in its new home - const amount: Position = patch(axis.line, mainAxisDistance, crossAxisDistance); - - // How far away it is on the page from where it needs to be - const diff: Position = subtract(amount, pageOffset); + // The difference between its old position and new position + const distance: Position = subtract(destination, draggedDimensionFragment.center); - // The final client offset - const client: Position = add(diff, clientOffset); + // Accounting for page, client and scroll container offsets + const netPageClientOffset: Position = subtract(clientOffset, pageOffset); + const offsets: Position = add(droppableScrollDiff, netPageClientOffset); - // Accounting for container scroll - const withScroll: Position = add(client, droppableScrollDiff); + // Finally, this is how far the dragged item has to travel to be in its new home + const withOffsets: Position = add(distance, offsets); - return withScroll; + return withOffsets; }; diff --git a/stories/src/multiple-vertical/quote-app.jsx b/stories/src/multiple-vertical/quote-app.jsx index 4acad5d1c6..d6ece527da 100644 --- a/stories/src/multiple-vertical/quote-app.jsx +++ b/stories/src/multiple-vertical/quote-app.jsx @@ -132,7 +132,6 @@ export default class QuoteApp extends Component { From 4d7bf249fc268840b0ef9c3f52c540ef258a6d5b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 29 Aug 2017 17:08:18 +1000 Subject: [PATCH 028/117] wip --- src/state/get-draggables-inside-droppable.js | 49 +++++---------- .../get-closest-draggable.js | 60 ++++++++++++------- src/state/move-to-best-droppable/index.js | 20 +++---- .../move-to-new-spot.js | 8 +-- src/state/move-to-edge.js | 1 - 5 files changed, 68 insertions(+), 70 deletions(-) diff --git a/src/state/get-draggables-inside-droppable.js b/src/state/get-draggables-inside-droppable.js index 91b008ee95..857ce8f3b6 100644 --- a/src/state/get-draggables-inside-droppable.js +++ b/src/state/get-draggables-inside-droppable.js @@ -2,43 +2,22 @@ import memoizeOne from 'memoize-one'; import { draggableMapToList } from './dimension-map-to-list'; import type { - Position, - DraggableId, - DimensionFragment, DraggableDimension, DroppableDimension, DraggableDimensionMap, } from '../types'; -const isOverDroppable = (target: Position, droppable: DroppableDimension): boolean => { - const fragment: DimensionFragment = droppable.page.withMargin; - const { top, right, bottom, left } = fragment; - - return target.x >= left && - target.x <= right && - target.y >= top && - target.y <= bottom; -}; - -type Dragging = {| - draggableId: DraggableId, - center: Position, -|} - -export default (droppable: DroppableDimension, - draggables: DraggableDimensionMap, - dragging?: Dragging, -): DraggableDimension[] => draggableMapToList(draggables) - .filter((draggable: DraggableDimension): boolean => { - if (dragging && dragging.draggableId === draggable.id) { - return isOverDroppable(dragging.center, droppable); - } - - return droppable.id === draggable.droppableId; - }) - // Dimensions are not guarenteed to be ordered in the same order as keys - // So we need to sort them so they are in the correct order - .sort((a: DraggableDimension, b: DraggableDimension): number => ( - a.page.withoutMargin.center[droppable.axis.line] - - b.page.withoutMargin.center[droppable.axis.line] - )); +export default memoizeOne( + (droppable: DroppableDimension, + draggables: DraggableDimensionMap, + ): DraggableDimension[] => draggableMapToList(draggables) + .filter((draggable: DraggableDimension): boolean => ( + droppable.id === draggable.droppableId + )) + // Dimensions are not guarenteed to be ordered in the same order as keys + // So we need to sort them so they are in the correct order + .sort((a: DraggableDimension, b: DraggableDimension): number => ( + a.page.withoutMargin.center[droppable.axis.line] - + b.page.withoutMargin.center[droppable.axis.line] + )) +); diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index 7f4aff09d7..5787525453 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -1,9 +1,12 @@ // @flow import { add, distance } from '../position'; +import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import type { Axis, Position, DraggableDimension, + DraggableDimensionMap, + DroppableDimension, } from '../../types'; type Args = {| @@ -11,30 +14,47 @@ type Args = {| center: Position, // how far the destination Droppable is scrolled scrollOffset: Position, - draggables: DraggableDimension[], + // the droppable that is being moved to + destination: DroppableDimension, + draggables: DraggableDimensionMap, |} +// TODO +const isVisible = (draggable: DraggableDimension, droppable: DroppableDimension) => true; + export default ({ axis, center, scrollOffset, + destination, draggables, -}: Args): DraggableDimension => - draggables.sort((a: DraggableDimension, b: DraggableDimension): number => { - const distanceToA = distance(center, add(a.page.withMargin.center, scrollOffset)); - const distanceToB = distance(center, add(b.page.withMargin.center, scrollOffset)); - - // if a is closer - return a - if (distanceToA < distanceToB) { - return -1; - } - - // if b is closer - return b - if (distanceToB < distanceToA) { - return 1; - } - - // if the distance to a and b are the same: - // return the one that appears first on the main axis - return a.page.withMargin[axis.start] - b.page.withMargin[axis.start]; - })[0]; +}: Args): ?DraggableDimension => { + const siblings: DraggableDimension[] = getDraggablesInsideDroppable( + destination, draggables + ); + + const result: DraggableDimension[] = + // remove any options that are hidden by overflow + siblings + .filter((draggable: DraggableDimension) => isVisible(draggable, destination)) + .sort((a: DraggableDimension, b: DraggableDimension): number => { + const distanceToA = distance(center, add(a.page.withMargin.center, scrollOffset)); + const distanceToB = distance(center, add(b.page.withMargin.center, scrollOffset)); + + // if a is closer - return a + if (distanceToA < distanceToB) { + return -1; + } + + // if b is closer - return b + if (distanceToB < distanceToA) { + return 1; + } + + // if the distance to a and b are the same: + // return the one that appears first on the main axis + return a.page.withMargin[axis.start] - b.page.withMargin[axis.start]; + }); + + return result.length ? result[0] : null; +}; diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index 085d2710a8..ce299c00b6 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -50,18 +50,18 @@ export default ({ return null; } - const newSiblings: DraggableDimension[] = getDraggablesInsideDroppable( + // const newSiblings: DraggableDimension[] = getDraggablesInsideDroppable( + // destination, + // draggables, + // ); + + const target: ?DraggableDimension = getClosestDraggable({ + axis: destination.axis, + center, + scrollOffset: destination.scroll.current, destination, draggables, - ); - - const target: ?DraggableDimension = newSiblings.length ? - getClosestDraggable({ - axis: destination.axis, - center, - scrollOffset: destination.scroll.current, - draggables: newSiblings, - }) : null; + }); return moveToNewSpot({ center, diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index f45f193285..abb2963bf3 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -92,10 +92,6 @@ export default ({ const isGoingBeforeTarget: boolean = center[sourceAxis.line] < target.page.withMargin.center[sourceAxis.line]; - console.log(`moving ${draggable.id}`); - console.log('source edge:', isGoingBeforeTarget ? 'end' : 'start'); - console.log('destination edge:', isGoingBeforeTarget ? 'start' : 'end'); - const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, // TODO: source edge will always be start - unless moving to home column? @@ -105,6 +101,10 @@ export default ({ destinationAxis, }); + // const targetIndex: number = getDraggablesInDroppable( + // target, dragg + // ); + // TODO const impact: DragImpact = { movement: { diff --git a/src/state/move-to-edge.js b/src/state/move-to-edge.js index 3329aa91f1..74f7f155fa 100644 --- a/src/state/move-to-edge.js +++ b/src/state/move-to-edge.js @@ -41,7 +41,6 @@ export default ({ // 1. Find the intersection corner point // 2. add the difference between that point and the center of the dimension - const corner: Position = getCorner(destination); // the difference between the center of the draggable and its corner From ac8f28fe2e1b42a661928ff9d771efa919e83a7e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 29 Aug 2017 21:18:37 +1000 Subject: [PATCH 029/117] wip tests --- stories/src/board/column.jsx | 2 +- stories/src/vertical/quote-list.jsx | 14 ++ test/unit/state/move-to-edge.spec.js | 193 +++++++++++++++++++++++++++ test/utils/get-client-rect.js | 1 + test/utils/get-fragment.js | 17 +++ 5 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 test/unit/state/move-to-edge.spec.js create mode 100644 test/utils/get-fragment.js diff --git a/stories/src/board/column.jsx b/stories/src/board/column.jsx index 100e7765de..19132344b4 100644 --- a/stories/src/board/column.jsx +++ b/stories/src/board/column.jsx @@ -64,7 +64,7 @@ export default class Column extends Component { diff --git a/stories/src/vertical/quote-list.jsx b/stories/src/vertical/quote-list.jsx index d590c46d12..04ea8a7f79 100644 --- a/stories/src/vertical/quote-list.jsx +++ b/stories/src/vertical/quote-list.jsx @@ -25,6 +25,16 @@ const Container = styled.div` width: 250px; `; +const AddCard = styled.button` + margin-bottom: ${grid}px; + margin-top: ${grid}px; + outline: none; + border: none; + font-size: 14px; + text-align: left; + padding: ${grid * 1.5}px ${grid}px; +`; + export default class QuoteList extends Component { props: {| listId: string, @@ -58,6 +68,10 @@ export default class QuoteList extends Component { )}
))} + {dropProvided.placeholder} + + Add a card... + )} diff --git a/test/unit/state/move-to-edge.spec.js b/test/unit/state/move-to-edge.spec.js new file mode 100644 index 0000000000..cb1ee8e269 --- /dev/null +++ b/test/unit/state/move-to-edge.spec.js @@ -0,0 +1,193 @@ +// @flow +import type { + Position, + DimensionFragment, +} from '../../../src/types'; +import getFragment from '../../utils/get-fragment'; +import getClientRect from '../../utils/get-client-rect'; +import moveToEdge from '../../../src/state/move-to-edge'; +import { vertical, horizontal } from '../../../src/state/axis'; + +// width: 40, height: 20 +const source: DimensionFragment = getFragment(getClientRect({ + top: 0, + left: 0, + right: 40, + bottom: 20, +})); + +// width: 50, height: 60 +const destination: DimensionFragment = getFragment(getClientRect({ + top: 50, + left: 50, + right: 100, + bottom: 110, +})); + +// All results are aligned on the crossAxisStart + +describe('move to edge', () => { + describe('moving to vertical list', () => { + describe('destination start edge', () => { + describe('to source end edge', () => { + it('should move to the correct position', () => { + const center: Position = { + x: 70, + y: 50, + }; + + const result: Position = moveToEdge({ + source, + sourceEdge: 'end', + destination, + destinationEdge: 'start', + destinationAxis: vertical, + }); + + expect(result).toEqual(center); + }); + }); + + describe('to source start edge', () => { + it('should move to the correct position', () => { + const center: Position = { + x: 70, + y: 70, + }; + + const result: Position = moveToEdge({ + source, + sourceEdge: 'start', + destination, + destinationEdge: 'start', + destinationAxis: vertical, + }); + + expect(result).toEqual(center); + }); + }); + }); + + describe('destination end edge', () => { + describe('to source end edge', () => { + it('should move to the correct position', () => { + const center: Position = { + x: 70, + y: 90, + }; + + const result: Position = moveToEdge({ + source, + sourceEdge: 'end', + destination, + destinationEdge: 'end', + destinationAxis: vertical, + }); + + expect(result).toEqual(center); + }); + }); + + describe('to source start edge', () => { + it('should move to the correct position', () => { + const center: Position = { + x: 70, + y: 110, + }; + + const result: Position = moveToEdge({ + source, + sourceEdge: 'start', + destination, + destinationEdge: 'end', + destinationAxis: vertical, + }); + + expect(result).toEqual(center); + }); + }); + }); + }); + + describe.skip('moving to horizontal list', () => { + describe('destination start edge', () => { + describe('to source end edge', () => { + it('should move to the correct position', () => { + const center: Position = { + x: 50, + y: 70, + }; + + const result: Position = moveToEdge({ + source, + sourceEdge: 'end', + destination, + destinationEdge: 'start', + destinationAxis: horizontal, + }); + + expect(result).toEqual(center); + }); + }); + + describe('to source start edge', () => { + it('should move to the correct position', () => { + const center: Position = { + x: 70, + y: 70, + }; + + const result: Position = moveToEdge({ + source, + sourceEdge: 'start', + destination, + destinationEdge: 'start', + destinationAxis: horizontal, + }); + + expect(result).toEqual(center); + }); + }); + }); + + describe('destination end edge', () => { + describe('to source end edge', () => { + it('should move to the correct position', () => { + const center: Position = { + x: 70, + y: 90, + }; + + const result: Position = moveToEdge({ + source, + sourceEdge: 'end', + destination, + destinationEdge: 'end', + destinationAxis: vertical, + }); + + expect(result).toEqual(center); + }); + }); + + describe('to source start edge', () => { + it('should move to the correct position', () => { + const center: Position = { + x: 70, + y: 110, + }; + + const result: Position = moveToEdge({ + source, + sourceEdge: 'start', + destination, + destinationEdge: 'end', + destinationAxis: vertical, + }); + + expect(result).toEqual(center); + }); + }); + }); + }); +}); diff --git a/test/utils/get-client-rect.js b/test/utils/get-client-rect.js index ccc4f3254b..80daefb764 100644 --- a/test/utils/get-client-rect.js +++ b/test/utils/get-client-rect.js @@ -1,3 +1,4 @@ +// @flow import type { ClientRect } from '../../src/state/dimension'; type GetClientRect = {| diff --git a/test/utils/get-fragment.js b/test/utils/get-fragment.js new file mode 100644 index 0000000000..9131272486 --- /dev/null +++ b/test/utils/get-fragment.js @@ -0,0 +1,17 @@ +// @flow +import type { DimensionFragment } from '../../src/types'; +import type { ClientRect } from '../../src/state/dimension'; + +export default (clientRect: ClientRect): DimensionFragment => { + const { top, left, bottom, right, width, height } = clientRect; + const center = { + x: (right + left) / 2, + y: (top + bottom) / 2, + }; + + const fragment: DimensionFragment = { + top, left, bottom, right, width, height, center, + }; + + return fragment; +}; From 7628ea2b9d30cd6b798a5ce44e777a8a2394aa69 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 30 Aug 2017 17:11:52 +1000 Subject: [PATCH 030/117] adding tests for move-to-edge --- src/state/move-to-edge.js | 4 +- test/unit/state/move-to-edge.spec.js | 337 +++++++++++++++++---------- 2 files changed, 211 insertions(+), 130 deletions(-) diff --git a/src/state/move-to-edge.js b/src/state/move-to-edge.js index 74f7f155fa..8f6f5eac06 100644 --- a/src/state/move-to-edge.js +++ b/src/state/move-to-edge.js @@ -35,6 +35,8 @@ export default ({ }: Args): CenterPosition => { const getCorner = (fragment: DimensionFragment): Position => patch( destinationAxis.line, + // it does not really matter what edge we use here + // as the difference to the center from edges will be the same fragment[destinationAxis[destinationEdge]], fragment[destinationAxis.crossAxisStart] ); @@ -51,7 +53,7 @@ export default ({ const signed: Position = patch( destinationAxis.line, - // if moving to the sourceEdge then we need to pull backwards on the main axis + // if moving to the end edge - we need to pull the source backwards (sourceEdge === 'end' ? -1 : 1) * centerDiff[destinationAxis.line], centerDiff[destinationAxis.crossLine], ); diff --git a/test/unit/state/move-to-edge.spec.js b/test/unit/state/move-to-edge.spec.js index cb1ee8e269..b1dc2ae7fa 100644 --- a/test/unit/state/move-to-edge.spec.js +++ b/test/unit/state/move-to-edge.spec.js @@ -1,21 +1,39 @@ // @flow -import type { - Position, - DimensionFragment, -} from '../../../src/types'; +import { + add, + absolute, + isEqual, + patch, + subtract, +} from '../../../src/state/position'; import getFragment from '../../utils/get-fragment'; import getClientRect from '../../utils/get-client-rect'; import moveToEdge from '../../../src/state/move-to-edge'; import { vertical, horizontal } from '../../../src/state/axis'; +import type { + Axis, + Position, + DimensionFragment, +} from '../../../src/types'; +// behind the destination // width: 40, height: 20 -const source: DimensionFragment = getFragment(getClientRect({ +const behind: DimensionFragment = getFragment(getClientRect({ top: 0, left: 0, right: 40, bottom: 20, })); +// in front of the destination +// width: 50, height: 10 +const infront: DimensionFragment = getFragment(getClientRect({ + top: 120, + left: 150, + right: 200, + bottom: 130, +})); + // width: 50, height: 60 const destination: DimensionFragment = getFragment(getClientRect({ top: 50, @@ -26,166 +44,227 @@ const destination: DimensionFragment = getFragment(getClientRect({ // All results are aligned on the crossAxisStart +const pullBackwardsOnMainAxis = (axis: Axis) => (point: Position) => patch( + axis.line, + -point[axis.line], + point[axis.crossLine] +); + +// returns the absolute difference of the center position +// to one of the corners on the axis.end. Choosing axis.end is arbitrary +const getCenterDiff = (axis: Axis) => (source: DimensionFragment): Position => { + const corner = patch( + axis.line, source[axis.end], source[axis.crossAxisStart] + ); + + const diff = absolute(subtract(source.center, corner)); + + (() => { + // a little check to ensure that our assumption that the distance between the edges + // and the axis.end is the same + const otherCorner = patch( + axis.line, source[axis.end], source[axis.crossAxisEnd] + ); + const otherDiff = absolute(subtract(source.center, otherCorner)); + + if (!isEqual(diff, otherDiff)) { + throw new Error('invalidation position assumption'); + } + })(); + + return diff; +}; + describe('move to edge', () => { - describe('moving to vertical list', () => { - describe('destination start edge', () => { - describe('to source end edge', () => { - it('should move to the correct position', () => { - const center: Position = { - x: 70, - y: 50, + [behind, infront].forEach((source: DimensionFragment) => { + describe(`source is ${source === behind ? 'behind' : 'infront of'} destination`, () => { + describe('moving to a vertical list', () => { + const pullUpwards = pullBackwardsOnMainAxis(vertical); + const centerDiff = getCenterDiff(vertical)(source); + + describe('destination start edge', () => { + const destinationTopCorner: Position = { + x: destination.left, + y: destination.top, }; - const result: Position = moveToEdge({ - source, - sourceEdge: 'end', - destination, - destinationEdge: 'start', - destinationAxis: vertical, + describe('to source end edge', () => { + it('should move the source above the destination', () => { + const newCenter: Position = add( + pullUpwards(centerDiff), + destinationTopCorner + ); + + const result: Position = moveToEdge({ + source, + sourceEdge: 'end', + destination, + destinationEdge: 'start', + destinationAxis: vertical, + }); + + expect(result).toEqual(newCenter); + }); }); - expect(result).toEqual(center); - }); - }); + describe('to source start edge', () => { + it('should move below the top of the destination', () => { + const newCenter: Position = add( + centerDiff, + destinationTopCorner, + ); - describe('to source start edge', () => { - it('should move to the correct position', () => { - const center: Position = { - x: 70, - y: 70, - }; + const result: Position = moveToEdge({ + source, + sourceEdge: 'start', + destination, + destinationEdge: 'start', + destinationAxis: vertical, + }); - const result: Position = moveToEdge({ - source, - sourceEdge: 'start', - destination, - destinationEdge: 'start', - destinationAxis: vertical, + expect(result).toEqual(newCenter); + }); }); - - expect(result).toEqual(center); }); - }); - }); - describe('destination end edge', () => { - describe('to source end edge', () => { - it('should move to the correct position', () => { - const center: Position = { - x: 70, - y: 90, + describe('destination end edge', () => { + const destinationBottomCorner: Position = { + x: destination.left, + y: destination.bottom, }; - const result: Position = moveToEdge({ - source, - sourceEdge: 'end', - destination, - destinationEdge: 'end', - destinationAxis: vertical, + describe('to source end edge', () => { + it('should move above the bottom of the destination', () => { + const newCenter: Position = add( + pullUpwards(centerDiff), + destinationBottomCorner, + ); + + const result: Position = moveToEdge({ + source, + sourceEdge: 'end', + destination, + destinationEdge: 'end', + destinationAxis: vertical, + }); + + expect(result).toEqual(newCenter); + }); }); - expect(result).toEqual(center); - }); - }); + describe('to source start edge', () => { + it('should move below the destination', () => { + const newCenter: Position = add( + centerDiff, + destinationBottomCorner, + ); - describe('to source start edge', () => { - it('should move to the correct position', () => { - const center: Position = { - x: 70, - y: 110, - }; + const result: Position = moveToEdge({ + source, + sourceEdge: 'start', + destination, + destinationEdge: 'end', + destinationAxis: vertical, + }); - const result: Position = moveToEdge({ - source, - sourceEdge: 'start', - destination, - destinationEdge: 'end', - destinationAxis: vertical, + expect(result).toEqual(newCenter); + }); }); - - expect(result).toEqual(center); }); }); - }); - }); - describe.skip('moving to horizontal list', () => { - describe('destination start edge', () => { - describe('to source end edge', () => { - it('should move to the correct position', () => { - const center: Position = { - x: 50, - y: 70, + describe('moving to a horizontal list', () => { + const pullLeft = pullBackwardsOnMainAxis(horizontal); + const centerDiff = getCenterDiff(horizontal)(source); + + describe('destination start edge', () => { + const destinationTopCorner: Position = { + x: destination.left, // axis.start + y: destination.top, // axis.crossAxisStart }; - const result: Position = moveToEdge({ - source, - sourceEdge: 'end', - destination, - destinationEdge: 'start', - destinationAxis: horizontal, + describe('to source end edge', () => { + it('should move the source to the left of destination start edge', () => { + const newCenter: Position = add( + pullLeft(centerDiff), + destinationTopCorner + ); + + const result: Position = moveToEdge({ + source, + sourceEdge: 'end', + destination, + destinationEdge: 'start', + destinationAxis: horizontal, + }); + + expect(result).toEqual(newCenter); + }); }); - expect(result).toEqual(center); - }); - }); + describe('to source start edge', () => { + it('should move to the right of the destination start edge', () => { + const newCenter: Position = add( + centerDiff, + destinationTopCorner, + ); - describe('to source start edge', () => { - it('should move to the correct position', () => { - const center: Position = { - x: 70, - y: 70, - }; + const result: Position = moveToEdge({ + source, + sourceEdge: 'start', + destination, + destinationEdge: 'start', + destinationAxis: horizontal, + }); - const result: Position = moveToEdge({ - source, - sourceEdge: 'start', - destination, - destinationEdge: 'start', - destinationAxis: horizontal, + expect(result).toEqual(newCenter); + }); }); - - expect(result).toEqual(center); }); - }); - }); - describe('destination end edge', () => { - describe('to source end edge', () => { - it('should move to the correct position', () => { - const center: Position = { - x: 70, - y: 90, + describe('destination end edge', () => { + const destinationTopRightCorner: Position = { + x: destination.right, // axis.end + y: destination.top, // axis.crossAxisStart }; - const result: Position = moveToEdge({ - source, - sourceEdge: 'end', - destination, - destinationEdge: 'end', - destinationAxis: vertical, + describe('to source end edge', () => { + it('should move to the left of right side of the destination', () => { + const newCenter: Position = add( + pullLeft(centerDiff), + destinationTopRightCorner, + ); + + const result: Position = moveToEdge({ + source, + sourceEdge: 'end', + destination, + destinationEdge: 'end', + destinationAxis: horizontal, + }); + + expect(result).toEqual(newCenter); + }); }); - expect(result).toEqual(center); - }); - }); + describe('to source start edge', () => { + it('should move to the right of the destination', () => { + const newCenter: Position = add( + centerDiff, + destinationTopRightCorner, + ); - describe('to source start edge', () => { - it('should move to the correct position', () => { - const center: Position = { - x: 70, - y: 110, - }; + const result: Position = moveToEdge({ + source, + sourceEdge: 'start', + destination, + destinationEdge: 'end', + destinationAxis: horizontal, + }); - const result: Position = moveToEdge({ - source, - sourceEdge: 'start', - destination, - destinationEdge: 'end', - destinationAxis: vertical, + expect(result).toEqual(newCenter); + }); }); - - expect(result).toEqual(center); }); }); }); From b0302949a1dfa26d2c3ef41dec59751827a8c8a5 Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Wed, 30 Aug 2017 17:20:35 +1000 Subject: [PATCH 031/117] refactor getNewHomeClientOffset tests to make life easier when adding multiple-list tests --- .../state/get-new-home-client-offset.spec.js | 482 ++++++++++-------- 1 file changed, 267 insertions(+), 215 deletions(-) diff --git a/test/unit/state/get-new-home-client-offset.spec.js b/test/unit/state/get-new-home-client-offset.spec.js index edccf86cb4..51ff8fd3e6 100644 --- a/test/unit/state/get-new-home-client-offset.spec.js +++ b/test/unit/state/get-new-home-client-offset.spec.js @@ -4,7 +4,6 @@ import noImpact from '../../../src/state/no-impact'; import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; import { add, negate, subtract } from '../../../src/state/position'; import getClientRect from '../../utils/get-client-rect'; -import { vertical, horizontal } from '../../../src/state/axis'; import type { DroppableId, DragMovement, @@ -14,14 +13,68 @@ import type { DroppableDimension, } from '../../../src/types'; +type Rect = {| + top: number, + left: number, + bottom: number, + right: number, +|}; + +type CreateDroppableArgs = {| + direction?: 'vertical' | 'horizontal', + droppableId: DroppableId, + droppableRect: Rect, + draggableRects: Rect[], +|}; + +type TestDroppable = { + droppableId: string, + droppable: DroppableDimension, + draggables: DraggableDimensionMap, + draggableIds: string[], + draggableDimensions: DraggableDimension[], +}; + const origin: Position = { x: 0, y: 0 }; -const droppableId: DroppableId = 'drop-1'; -let draggable1: DraggableDimension; -let draggable2: DraggableDimension; -let draggable3: DraggableDimension; -let draggables: DraggableDimensionMap; -let draggableId; -let destinationDroppable: DroppableDimension; + +const createDroppable = ({ + direction = 'vertical', + droppableId, + droppableRect, + draggableRects, +}: CreateDroppableArgs): TestDroppable => { + const droppable = getDroppableDimension({ + id: droppableId, + direction, + clientRect: getClientRect(droppableRect), + }); + + const draggableDimensions = draggableRects.map( + (draggableRect, index) => getDraggableDimension({ + id: `${droppableId}::drag-${index}`, + droppableId, + clientRect: getClientRect(draggableRect), + }) + ); + + const draggables = draggableDimensions.reduce( + (currentDraggables, draggable) => ({ + ...currentDraggables, + [draggable.id]: draggable, + }), + {} + ); + + const draggableIds = Object.keys(draggables); + + return { + droppableId, + droppable, + draggables, + draggableIds, + draggableDimensions, + }; +}; const getDistanceOverDraggables = dimension => arr => ({ [dimension === 'height' ? 'y' : 'x']: arr.reduce( @@ -34,59 +87,19 @@ const getVerticalDistanceOverDraggables = getDistanceOverDraggables('height'); const getHorizontalDistanceOverDraggables = getDistanceOverDraggables('width'); describe('get new home client offset', () => { + let droppable; + beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => { }); - draggable1 = getDraggableDimension({ - id: 'drag-1', - droppableId, - clientRect: getClientRect({ - top: 0, - left: 0, - bottom: 100, - right: 100, - }), - }); - // huge height: 199 - draggable2 = getDraggableDimension({ - id: 'drag-2', - droppableId, - clientRect: getClientRect({ - top: 101, - left: 0, - bottom: 300, - right: 100, - }), - }); - - // height: 299 - draggable3 = getDraggableDimension({ - id: 'drag-3', - droppableId, - clientRect: getClientRect({ - top: 301, - left: 0, - bottom: 600, - right: 100, - }), - }); - - draggables = { - [draggable1.id]: draggable1, - [draggable2.id]: draggable2, - [draggable3.id]: draggable3, - }; - - draggableId = Object.keys(draggables)[0]; - - destinationDroppable = getDroppableDimension({ - id: droppableId, - clientRect: getClientRect({ - top: 0, - left: 0, - bottom: 600, - right: 100, - }), + droppable = createDroppable({ + droppableId: 'drop-1', + droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, + draggableRects: [ + { top: 0, left: 0, bottom: 100, right: 100 }, + { top: 101, left: 0, bottom: 300, right: 100 }, + { top: 301, left: 0, bottom: 600, right: 100 }, + ], }); }); @@ -115,9 +128,9 @@ describe('get new home client offset', () => { pageOffset: offset, droppableScrollDiff, windowScrollDiff, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], + destinationDroppable: droppable.droppable, }); expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); @@ -139,10 +152,10 @@ describe('get new home client offset', () => { // There should be no movement without an axis // This is an error situation const movement: DragMovement = { - draggables: [draggable2.id, draggable3.id], + draggables: droppable.draggableIds.slice(1), amount: { x: 0, - y: draggable1.page.withMargin.height, + y: droppable.draggableDimensions[0].page.withMargin.height, }, isBeyondStartPosition: true, }; @@ -153,8 +166,8 @@ describe('get new home client offset', () => { pageOffset: offset, droppableScrollDiff, windowScrollDiff, - draggables, - draggableId, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], destinationDroppable: null, }); @@ -169,31 +182,36 @@ describe('get new home client offset', () => { beforeEach(() => { selection = { - x: draggable1.client.withoutMargin.center.x, - y: draggable3.client.withoutMargin.top + 1, + x: droppable.draggableDimensions[0].client.withoutMargin.center.x, + y: droppable.draggableDimensions[2].client.withoutMargin.top + 1, }; }); // moving the first item down past the third item it('should account for the current client location of the dragging item', () => { // The offset needed to get to the selection. - const clientOffset: Position = subtract(selection, draggable1.client.withoutMargin.center); + const clientOffset: Position = subtract( + selection, + droppable.draggableDimensions[0].client.withoutMargin.center + ); // this test does not exercise page movement const pageOffset: Position = clientOffset; const movement: DragMovement = { - draggables: [draggable2.id, draggable3.id], + draggables: droppable.draggableIds.slice(1), amount: { x: 0, - y: draggable1.page.withMargin.height, + y: droppable.draggableDimensions[0].page.withMargin.height, }, isBeyondStartPosition: true, }; // How much distance the item needs to travel to be in its new home // from where it started - const verticalChange = getVerticalDistanceOverDraggables([draggable2, draggable3]); + const verticalChange = getVerticalDistanceOverDraggables( + droppable.draggableDimensions.slice(1) + ); // How far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); // this is the final client offset @@ -205,9 +223,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff: origin, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -219,18 +237,20 @@ describe('get new home client offset', () => { const pageOffset: Position = origin; const droppableScrollDiff: Position = subtract( selection, - draggable1.page.withoutMargin.center + droppable.draggableDimensions[0].page.withoutMargin.center ); const movement: DragMovement = { - draggables: [draggable2.id, draggable3.id], + draggables: droppable.draggableIds.slice(1), amount: { x: 0, - y: draggable1.page.withMargin.height, + y: droppable.draggableDimensions[0].page.withMargin.height, }, isBeyondStartPosition: true, }; // this is where it needs to end up - const verticalChange = getVerticalDistanceOverDraggables([draggable2, draggable3]); + const verticalChange = getVerticalDistanceOverDraggables( + droppable.draggableDimensions.slice(1) + ); // this is how far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); // this is the final client offset @@ -242,9 +262,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -260,15 +280,17 @@ describe('get new home client offset', () => { const droppableScrollDiff = origin; const windowScrollDiff = pageOffset; const movement: DragMovement = { - draggables: [draggable2.id, draggable3.id], + draggables: droppable.draggableIds.slice(1), amount: { x: 0, - y: draggable1.page.withMargin.height, + y: droppable.draggableDimensions[0].page.withMargin.height, }, isBeyondStartPosition: true, }; // this is where it needs to end up - const verticalChange = getVerticalDistanceOverDraggables([draggable2, draggable3]); + const verticalChange = getVerticalDistanceOverDraggables( + droppable.draggableDimensions.slice(1) + ); // this is how far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); // this is the final client offset @@ -280,9 +302,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff: origin, windowScrollDiff, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -295,26 +317,28 @@ describe('get new home client offset', () => { let selection: Position; beforeEach(() => { - draggableId = Object.keys(draggables)[Object.keys(draggables).length - 1]; selection = { - x: draggable3.client.withoutMargin.center.x, - y: draggable1.client.withoutMargin.bottom - 1, + x: droppable.draggableDimensions[2].client.withoutMargin.center.x, + y: droppable.draggableDimensions[0].client.withoutMargin.bottom - 1, }; }); // moving the third item backwards past the first and second item it('should account for the current client location of the dragging item', () => { // The offset needed to get to the selection. - const clientOffset: Position = subtract(selection, draggable3.client.withoutMargin.center); + const clientOffset: Position = subtract( + selection, + droppable.draggableDimensions[2].client.withoutMargin.center + ); // this test does not exercise page movement const pageOffset: Position = clientOffset; const movement: DragMovement = { - draggables: [draggable1.id, draggable2.id], + draggables: droppable.draggableIds.slice(0, 2), amount: { x: 0, - y: draggable3.page.withMargin.height, + y: droppable.draggableDimensions[2].page.withMargin.height, }, isBeyondStartPosition: false, }; @@ -322,7 +346,7 @@ describe('get new home client offset', () => { // How much distance the item needs to travel to be in its new home // from where it started const verticalChange = negate( - getVerticalDistanceOverDraggables([draggable1, draggable2]) + getVerticalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) ); // How far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); @@ -335,9 +359,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff: origin, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[2], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -348,19 +372,22 @@ describe('get new home client offset', () => { // have not moved the item on the screen at all const clientOffset: Position = origin; // the window has scrolled to get it to the selection point - const pageOffset: Position = subtract(selection, draggable3.page.withoutMargin.center); + const pageOffset: Position = subtract( + selection, + droppable.draggableDimensions[2].page.withoutMargin.center + ); const movement: DragMovement = { - draggables: [draggable1.id, draggable2.id], + draggables: droppable.draggableIds.slice(0, 2), amount: { x: 0, - y: draggable3.page.withMargin.height, + y: droppable.draggableDimensions[2].page.withMargin.height, }, isBeyondStartPosition: false, }; // How much distance the item needs to travel to be in its new home // from where it started const verticalChange = negate( - getVerticalDistanceOverDraggables([draggable1, draggable2]) + getVerticalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) ); // How far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); @@ -373,9 +400,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff: origin, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[2], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -387,19 +414,19 @@ describe('get new home client offset', () => { const pageOffset: Position = origin; const droppableScrollDiff: Position = subtract( selection, - draggable3.page.withoutMargin.center + droppable.draggableDimensions[2].page.withoutMargin.center ); const movement: DragMovement = { - draggables: [draggable1.id, draggable2.id], + draggables: droppable.draggableIds.slice(0, 2), amount: { x: 0, - y: draggable3.page.withMargin.height, + y: droppable.draggableDimensions[2].page.withMargin.height, }, isBeyondStartPosition: false, }; // this is where it needs to end up const verticalChange = negate( - getVerticalDistanceOverDraggables([draggable1, draggable2]) + getVerticalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) ); // this is how far away it is from where it needs to end up const diff: Position = subtract(verticalChange, pageOffset); @@ -412,9 +439,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[2], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -425,59 +452,71 @@ describe('get new home client offset', () => { describe('horizontal', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => { }); - draggable1 = getDraggableDimension({ - id: 'drag-1', - droppableId, - clientRect: getClientRect({ - top: 0, - left: 0, - bottom: 100, - right: 100, - }), - }); - // huge height: 199 - draggable2 = getDraggableDimension({ - id: 'drag-2', - droppableId, - clientRect: getClientRect({ - top: 0, - left: 101, - bottom: 100, - right: 300, - }), - }); - - // height: 299 - draggable3 = getDraggableDimension({ - id: 'drag-3', - droppableId, - clientRect: getClientRect({ - top: 0, - left: 301, - bottom: 100, - right: 500, - }), - }); - - draggables = { - [draggable1.id]: draggable1, - [draggable2.id]: draggable2, - [draggable3.id]: draggable3, - }; - - draggableId = Object.keys(draggables)[0]; - - destinationDroppable = getDroppableDimension({ - id: droppableId, + droppable = createDroppable({ direction: 'horizontal', - clientRect: getClientRect({ - top: 0, - left: 0, - bottom: 100, - right: 500, - }), + droppableId: 'drop-1', + droppableRect: { top: 0, left: 0, bottom: 100, right: 500 }, + draggableRects: [ + { top: 0, left: 0, bottom: 100, right: 100 }, + { top: 0, left: 101, bottom: 100, right: 300 }, + { top: 0, left: 301, bottom: 100, right: 500 }, + ], }); + + // draggable1 = getDraggableDimension({ + // id: 'drag-1', + // droppableId, + // clientRect: getClientRect({ + // top: 0, + // left: 0, + // bottom: 100, + // right: 100, + // }), + // }); + + // // huge height: 199 + // draggable2 = getDraggableDimension({ + // id: 'drag-2', + // droppableId, + // clientRect: getClientRect({ + // top: 0, + // left: 101, + // bottom: 100, + // right: 300, + // }), + // }); + + // // height: 299 + // draggable3 = getDraggableDimension({ + // id: 'drag-3', + // droppableId, + // clientRect: getClientRect({ + // top: 0, + // left: 301, + // bottom: 100, + // right: 500, + // }), + // }); + + // draggables = { + // [draggable1.id]: draggable1, + // [draggable2.id]: draggable2, + // [draggable3.id]: draggable3, + // }; + + // draggableId = Object.keys(draggables)[0]; + + // destinationDroppable = getDroppableDimension({ + // id: droppableId, + // direction: 'horizontal', + // clientRect: getClientRect({ + // top: 0, + // left: 0, + // bottom: 100, + // right: 500, + // }), + // }); }); it('should return to the total scroll diff if nothing has moved', () => { @@ -500,9 +539,9 @@ describe('get new home client offset', () => { pageOffset: offset, droppableScrollDiff, windowScrollDiff, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], + destinationDroppable: droppable.droppable, }); expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); @@ -524,9 +563,9 @@ describe('get new home client offset', () => { // There should be no movement without an axis // This is an error situation const movement: DragMovement = { - draggables: [draggable2.id, draggable3.id], + draggables: droppable.draggableIds.slice(1), amount: { - x: draggable1.page.withMargin.width, + x: droppable.draggableDimensions[0].page.withMargin.width, y: 0, }, isBeyondStartPosition: true, @@ -538,8 +577,8 @@ describe('get new home client offset', () => { pageOffset: offset, droppableScrollDiff, windowScrollDiff, - draggables, - draggableId, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], destinationDroppable: null, }); @@ -554,23 +593,26 @@ describe('get new home client offset', () => { beforeEach(() => { selection = { - x: draggable3.client.withoutMargin.left + 1, - y: draggable1.client.withoutMargin.center.y, + x: droppable.draggableDimensions[2].client.withoutMargin.left + 1, + y: droppable.draggableDimensions[0].client.withoutMargin.center.y, }; }); // moving the first item down past the third item it('should account for the current client location of the dragging item', () => { // The offset needed to get to the selection. - const clientOffset: Position = subtract(selection, draggable1.client.withoutMargin.center); + const clientOffset: Position = subtract( + selection, + droppable.draggableDimensions[0].client.withoutMargin.center + ); // this test does not exercise page movement const pageOffset: Position = clientOffset; const movement: DragMovement = { - draggables: [draggable2.id, draggable3.id], + draggables: droppable.draggableIds.slice(1), amount: { - x: draggable1.page.withMargin.width, + x: droppable.draggableDimensions[0].page.withMargin.width, y: 0, }, isBeyondStartPosition: true, @@ -578,7 +620,9 @@ describe('get new home client offset', () => { // How much distance the item needs to travel to be in its new home // from where it started - const horizontalChange = getHorizontalDistanceOverDraggables([draggable2, draggable3]); + const horizontalChange = getHorizontalDistanceOverDraggables( + droppable.draggableDimensions.slice(1) + ); // How far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); // this is the final client offset @@ -590,9 +634,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff: origin, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -604,18 +648,20 @@ describe('get new home client offset', () => { const pageOffset: Position = origin; const droppableScrollDiff: Position = subtract( selection, - draggable1.page.withoutMargin.center + droppable.draggableDimensions[0].page.withoutMargin.center ); const movement: DragMovement = { - draggables: [draggable2.id, draggable3.id], + draggables: droppable.draggableIds.slice(1), amount: { - x: draggable1.page.withMargin.width, + x: droppable.draggableDimensions[0].page.withMargin.width, y: 0, }, isBeyondStartPosition: true, }; // this is where it needs to end up - const horizontalChange = getHorizontalDistanceOverDraggables([draggable2, draggable3]); + const horizontalChange = getHorizontalDistanceOverDraggables( + droppable.draggableDimensions.slice(1) + ); // this is how far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); // this is the final client offset @@ -627,9 +673,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -645,15 +691,17 @@ describe('get new home client offset', () => { const droppableScrollDiff = origin; const windowScrollDiff = pageOffset; const movement: DragMovement = { - draggables: [draggable2.id, draggable3.id], + draggables: droppable.draggableIds.slice(1), amount: { - x: draggable1.page.withMargin.width, + x: droppable.draggableDimensions[0].page.withMargin.width, y: 0, }, isBeyondStartPosition: true, }; // this is where it needs to end up - const horizontalChange = getHorizontalDistanceOverDraggables([draggable2, draggable3]); + const horizontalChange = getHorizontalDistanceOverDraggables( + droppable.draggableDimensions.slice(1) + ); // this is how far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); // this is the final client offset @@ -665,9 +713,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff: origin, windowScrollDiff, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[0], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -681,26 +729,27 @@ describe('get new home client offset', () => { let selection: Position; beforeEach(() => { - draggableId = Object.keys(draggables)[Object.keys(draggables).length - 1]; - selection = { - x: draggable1.client.withoutMargin.right - 1, - y: draggable3.client.withoutMargin.center.y, + x: droppable.draggableDimensions[0].client.withoutMargin.right - 1, + y: droppable.draggableDimensions[2].client.withoutMargin.center.y, }; }); // moving the third item back past the first and second item it('should account for the current client location of the dragging item', () => { // The offset needed to get to the selection. - const clientOffset: Position = subtract(selection, draggable3.client.withoutMargin.center); + const clientOffset: Position = subtract( + selection, + droppable.draggableDimensions[2].client.withoutMargin.center + ); // this test does not exercise page movement const pageOffset: Position = clientOffset; const movement: DragMovement = { - draggables: [draggable1.id, draggable2.id], + draggables: droppable.draggableIds.slice(0, 2), amount: { - x: draggable3.page.withMargin.width, + x: droppable.draggableDimensions[2].page.withMargin.width, y: 0, }, isBeyondStartPosition: false, @@ -709,7 +758,7 @@ describe('get new home client offset', () => { // How much distance the item needs to travel to be in its new home // from where it started const horizontalChange = negate( - getHorizontalDistanceOverDraggables([draggable1, draggable2]) + getHorizontalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) ); // How far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); @@ -722,9 +771,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff: origin, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[2], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -735,11 +784,14 @@ describe('get new home client offset', () => { // have not moved the item on the screen at all const clientOffset: Position = origin; // the window has scrolled to get it to the selection point - const pageOffset: Position = subtract(selection, draggable3.page.withoutMargin.center); + const pageOffset: Position = subtract( + selection, + droppable.draggableDimensions[2].page.withoutMargin.center + ); const movement: DragMovement = { - draggables: [draggable1.id, draggable2.id], + draggables: droppable.draggableIds.slice(0, 2), amount: { - x: draggable3.page.withMargin.width, + x: droppable.draggableDimensions[2].page.withMargin.width, y: 0, }, isBeyondStartPosition: false, @@ -747,7 +799,7 @@ describe('get new home client offset', () => { // How much distance the item needs to travel to be in its new home // from where it started const horizontalChange = negate( - getHorizontalDistanceOverDraggables([draggable1, draggable2]) + getHorizontalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) ); // How far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); @@ -760,9 +812,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff: origin, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[2], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); @@ -774,19 +826,19 @@ describe('get new home client offset', () => { const pageOffset: Position = origin; const droppableScrollDiff: Position = subtract( selection, - draggable3.page.withoutMargin.center + droppable.draggableDimensions[2].page.withoutMargin.center ); const movement: DragMovement = { - draggables: [draggable1.id, draggable2.id], + draggables: droppable.draggableIds.slice(0, 2), amount: { - x: draggable3.page.withMargin.width, + x: droppable.draggableDimensions[2].page.withMargin.width, y: 0, }, isBeyondStartPosition: false, }; // this is where it needs to end up const horizontalChange = negate( - getHorizontalDistanceOverDraggables([draggable1, draggable2]) + getHorizontalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) ); // this is how far away it is from where it needs to end up const diff: Position = subtract(horizontalChange, pageOffset); @@ -799,9 +851,9 @@ describe('get new home client offset', () => { pageOffset, droppableScrollDiff, windowScrollDiff: origin, - draggables, - draggableId, - destinationDroppable, + draggables: droppable.draggables, + draggableId: droppable.draggableIds[2], + destinationDroppable: droppable.droppable, }); expect(newHomeOffset).toEqual(expected); From a1f0e4fe86ae53cab637285a2b752a6b415e33be Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 31 Aug 2017 14:02:21 +1000 Subject: [PATCH 032/117] refactoring keyboard movement - wip --- src/state/jump-to-next-index.js | 97 +++++++++++++++++++++++++-------- src/state/reducer.js | 12 ++-- 2 files changed, 79 insertions(+), 30 deletions(-) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index 90ca22b9e6..ad90a16541 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -1,7 +1,12 @@ // @flow import memoizeOne from 'memoize-one'; import getDraggablesInsideDroppable from './get-draggables-inside-droppable'; -import { patch } from './position'; +import { + add, + patch, + subtract, +} from './position'; +import moveToEdge from './move-to-edge'; import type { DraggableLocation, DraggableDimension, @@ -12,6 +17,7 @@ import type { DraggableId, Axis, DragImpact, + DimensionFragment, } from '../types'; const getIndex = memoizeOne( @@ -31,15 +37,28 @@ type JumpToNextArgs = {| |} export type JumpToNextResult = {| - diff: Position, + center: Position, impact: DragImpact, |} +type ShiftPosition = (point: Position, size: number, axis: Axis) => Position; + +const shift = (adjust: (original: Position, modification: Position) => Position): ShiftPosition => + (point: Position, size: number, axis: Axis): Position => { + const amount: Position = patch(axis.line, size); + + return adjust(point, amount); + }; + +// const pull = + +// const pull: ShiftPosition = shift(subtract, size: number); +// const push: ShiftPosition = shift(add, size: number); + export default ({ isMovingForward, draggableId, impact, - center, draggables, droppables, }: JumpToNextArgs): ?JumpToNextResult => { @@ -51,48 +70,79 @@ export default ({ const location: DraggableLocation = impact.destination; const droppable: DroppableDimension = droppables[location.droppableId]; const draggable: DraggableDimension = draggables[draggableId]; - const currentIndex: number = location.index; const axis: Axis = droppable.axis; const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( droppable, draggables, - { draggableId, - center, - } ); const startIndex: number = getIndex(insideDroppable, draggable); + const currentIndex: number = location.index; + const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; if (startIndex === -1) { console.error('could not find draggable inside current droppable'); return null; } - // cannot move beyond the last item - if (isMovingForward && currentIndex === insideDroppable.length - 1) { + // cannot move forward beyond the last item + if (proposedIndex > insideDroppable.length - 1) { return null; } // cannot move before the first item - if (!isMovingForward && currentIndex === 0) { + if (proposedIndex < 0) { return null; } + const destination: DraggableDimension = insideDroppable[proposedIndex]; const atCurrentIndex: DraggableDimension = insideDroppable[currentIndex]; - const nextIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; - const atNextIndex: DraggableDimension = insideDroppable[nextIndex]; - const isMovingTowardStart = (isMovingForward && nextIndex <= startIndex) || - (!isMovingForward && nextIndex >= startIndex); + // if moving forward: move start edge of source to end edge of destination + // if moving backward: move end edge of source to start edge of destination + + const center = moveToEdge({ + source: draggable.page.withoutMargin, + sourceEdge: isMovingForward ? 'start' : 'end', + destination: destination.page.withMargin, + destinationEdge: isMovingForward ? 'start' : 'end', + destinationAxis: droppable.axis, + }); + + const isMovingTowardStart = (isMovingForward && proposedIndex <= startIndex) || + (!isMovingForward && proposedIndex >= startIndex); + + const sizeDiff: Position = patch( + axis.line, + destination.page.withMargin[axis.size] - draggable.page.withMargin[axis.size] + ); + + const shifted = add(center, sizeDiff); + + // const shifted = (() => { + // const fragment: DimensionFragment = isMovingTowardStart ? + // atCurrentIndex.page.withMargin : + // draggable.page.withMargin; + + // const amount: Position = patch( + // axis.line, + // fragment[axis.size], + // ); + + // if (isMovingForward) { + // return subtract(center, amount); + // } + // return add(center, amount); + // })(); - const distance: number = isMovingTowardStart ? - atCurrentIndex.page.withMargin[axis.size] : - atNextIndex.page.withMargin[axis.size]; + // const distance: number = isMovingTowardStart ? + // atCurrentIndex.page.withMargin[axis.size] : + // destination.page.withMargin[axis.size]; - const signed: number = isMovingForward ? distance : -distance; + // const signed: number = isMovingForward ? distance : -distance; - const diff: Position = patch(axis.line, signed); + // const diff: Position = patch(axis.line, signed); // Calculate DragImpact @@ -102,24 +152,25 @@ export default ({ // we need to add the next draggable to the impact const moved: DraggableId[] = isMovingTowardStart ? impact.movement.draggables.slice(0, impact.movement.draggables.length - 1) : - [...impact.movement.draggables, atNextIndex.id]; + [...impact.movement.draggables, destination.id]; const newImpact: DragImpact = { movement: { draggables: moved, // The amount of movement will always be the size of the dragging item amount: patch(axis.line, draggable.page.withMargin[axis.size]), - isBeyondStartPosition: nextIndex > startIndex, + isBeyondStartPosition: proposedIndex > startIndex, }, destination: { droppableId: droppable.id, - index: nextIndex, + index: proposedIndex, }, direction: droppable.axis.direction, }; const result: JumpToNextResult = { - diff, impact: newImpact, + center: shifted, + impact: newImpact, }; return result; diff --git a/src/state/reducer.js b/src/state/reducer.js index 6c11d4fb26..d871e48571 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -403,7 +403,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { isMovingForward, draggableId: existing.current.id, impact: existing.impact, - center: existing.current.page.center, draggables: state.dimension.draggable, droppables: state.dimension.droppable, }); @@ -413,15 +412,14 @@ export default (state: State = clean('IDLE'), action: Action): State => { return state; } - const diff: Position = result.diff; const impact: DragImpact = result.impact; - const page: Position = add(existing.current.page.selection, diff); - const client: Position = add(existing.current.client.selection, diff); + // const page: Position = add(existing.current.page.selection, diff); + // const client: Position = add(existing.current.client.selection, diff); // current limitation: cannot go beyond visible border of list const droppableId: ?DroppableId = getDroppableOver( - page, state.dimension.droppable, + result.center, state.dimension.droppable, ); if (!droppableId) { @@ -433,8 +431,8 @@ export default (state: State = clean('IDLE'), action: Action): State => { return move({ state, impact, - clientSelection: client, - pageSelection: page, + clientSelection: result.center, + pageSelection: result.center, shouldAnimate: true, }); } From 8e2ab453e9dca3acdd177c0f81ba6b798adb5e3a Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Fri, 1 Sep 2017 14:05:37 +1000 Subject: [PATCH 033/117] add tests for updated getNewHomeClientOffset function. add client dimensions to DroppableDimension shape. --- src/state/dimension.js | 4 + src/state/get-new-home-client-offset.js | 10 +- src/types.js | 16 +- .../state/get-new-home-client-offset.spec.js | 379 ++++++++++++------ 4 files changed, 272 insertions(+), 137 deletions(-) diff --git a/src/state/dimension.js b/src/state/dimension.js index aa316860e0..a5b4eb54a3 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -139,6 +139,10 @@ export const getDroppableDimension = ({ // when we start the current scroll is the initial scroll current: scroll, }, + client: { + withoutMargin: getFragment(clientRect), + withMargin: getFragment(getWithMargin(clientRect, margin)), + }, page: { withoutMargin: getFragment(withWindowScroll), withMargin: getFragment(withWindowScrollAndMargin), diff --git a/src/state/get-new-home-client-offset.js b/src/state/get-new-home-client-offset.js index 48c5c241bc..7c7c484960 100644 --- a/src/state/get-new-home-client-offset.js +++ b/src/state/get-new-home-client-offset.js @@ -43,17 +43,15 @@ export default ({ ); // If there's no destination or if no movement has occurred, return the starting position. - if ( - !destinationDroppable || - (isWithinHomeDroppable && !movedDraggables.length) - ) { + if (!destinationDroppable || + (isWithinHomeDroppable && !movedDraggables.length)) { return add(droppableScrollDiff, windowScrollDiff); } const { axis, id: destinationDroppableId, - page: destinationDroppablePage, + client: destinationDroppableClient, } = destinationDroppable; // All the draggables in the destination (even the ones that haven't moved) @@ -85,7 +83,7 @@ export default ({ } // Otherwise, return the dimension of the empty droppable - return destinationDroppablePage.withMargin; + return destinationDroppableClient.withMargin; })(); const { sourceEdge, destinationEdge } = (() => { diff --git a/src/types.js b/src/types.js index 5b2201f13e..decb50fcc7 100644 --- a/src/types.js +++ b/src/types.js @@ -54,14 +54,16 @@ export type DimensionFragment = {| export type DraggableDimension = {| id: DraggableId, droppableId: DroppableId, - page: {| + // relative to the current viewport + client: {| withMargin: DimensionFragment, withoutMargin: DimensionFragment, |}, - client: {| + // relative to the whole page + page: {| withMargin: DimensionFragment, withoutMargin: DimensionFragment, - |} + |}, |} export type DroppableDimension = {| @@ -71,10 +73,16 @@ export type DroppableDimension = {| initial: Position, current: Position, |}, + // relative to the current viewport + client: {| + withMargin: DimensionFragment, + withoutMargin: DimensionFragment, + |}, + // relative to the whole page page: {| withMargin: DimensionFragment, withoutMargin: DimensionFragment, - |} + |}, |} export type DraggableLocation = {| droppableId: DroppableId, diff --git a/test/unit/state/get-new-home-client-offset.spec.js b/test/unit/state/get-new-home-client-offset.spec.js index 51ff8fd3e6..4a588332d4 100644 --- a/test/unit/state/get-new-home-client-offset.spec.js +++ b/test/unit/state/get-new-home-client-offset.spec.js @@ -87,12 +87,8 @@ const getVerticalDistanceOverDraggables = getDistanceOverDraggables('height'); const getHorizontalDistanceOverDraggables = getDistanceOverDraggables('width'); describe('get new home client offset', () => { - let droppable; - - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(() => { }); - - droppable = createDroppable({ + describe('vertical', () => { + const droppable = createDroppable({ droppableId: 'drop-1', droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, draggableRects: [ @@ -101,13 +97,7 @@ describe('get new home client offset', () => { { top: 301, left: 0, bottom: 600, right: 100 }, ], }); - }); - - afterEach(() => { - console.error.mockRestore(); - }); - describe('vertical', () => { it('should return the total scroll diff if nothing has moved', () => { const offset: Position = { x: 100, @@ -200,10 +190,7 @@ describe('get new home client offset', () => { const movement: DragMovement = { draggables: droppable.draggableIds.slice(1), - amount: { - x: 0, - y: droppable.draggableDimensions[0].page.withMargin.height, - }, + amount: origin, isBeyondStartPosition: true, }; @@ -241,10 +228,7 @@ describe('get new home client offset', () => { ); const movement: DragMovement = { draggables: droppable.draggableIds.slice(1), - amount: { - x: 0, - y: droppable.draggableDimensions[0].page.withMargin.height, - }, + amount: origin, isBeyondStartPosition: true, }; // this is where it needs to end up @@ -281,10 +265,7 @@ describe('get new home client offset', () => { const windowScrollDiff = pageOffset; const movement: DragMovement = { draggables: droppable.draggableIds.slice(1), - amount: { - x: 0, - y: droppable.draggableDimensions[0].page.withMargin.height, - }, + amount: origin, isBeyondStartPosition: true, }; // this is where it needs to end up @@ -336,10 +317,7 @@ describe('get new home client offset', () => { const movement: DragMovement = { draggables: droppable.draggableIds.slice(0, 2), - amount: { - x: 0, - y: droppable.draggableDimensions[2].page.withMargin.height, - }, + amount: origin, isBeyondStartPosition: false, }; @@ -378,10 +356,7 @@ describe('get new home client offset', () => { ); const movement: DragMovement = { draggables: droppable.draggableIds.slice(0, 2), - amount: { - x: 0, - y: droppable.draggableDimensions[2].page.withMargin.height, - }, + amount: origin, isBeyondStartPosition: false, }; // How much distance the item needs to travel to be in its new home @@ -418,10 +393,7 @@ describe('get new home client offset', () => { ); const movement: DragMovement = { draggables: droppable.draggableIds.slice(0, 2), - amount: { - x: 0, - y: droppable.draggableDimensions[2].page.withMargin.height, - }, + amount: origin, isBeyondStartPosition: false, }; // this is where it needs to end up @@ -450,73 +422,15 @@ describe('get new home client offset', () => { }); describe('horizontal', () => { - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(() => { }); - - droppable = createDroppable({ - direction: 'horizontal', - droppableId: 'drop-1', - droppableRect: { top: 0, left: 0, bottom: 100, right: 500 }, - draggableRects: [ - { top: 0, left: 0, bottom: 100, right: 100 }, - { top: 0, left: 101, bottom: 100, right: 300 }, - { top: 0, left: 301, bottom: 100, right: 500 }, - ], - }); - - // draggable1 = getDraggableDimension({ - // id: 'drag-1', - // droppableId, - // clientRect: getClientRect({ - // top: 0, - // left: 0, - // bottom: 100, - // right: 100, - // }), - // }); - - // // huge height: 199 - // draggable2 = getDraggableDimension({ - // id: 'drag-2', - // droppableId, - // clientRect: getClientRect({ - // top: 0, - // left: 101, - // bottom: 100, - // right: 300, - // }), - // }); - - // // height: 299 - // draggable3 = getDraggableDimension({ - // id: 'drag-3', - // droppableId, - // clientRect: getClientRect({ - // top: 0, - // left: 301, - // bottom: 100, - // right: 500, - // }), - // }); - - // draggables = { - // [draggable1.id]: draggable1, - // [draggable2.id]: draggable2, - // [draggable3.id]: draggable3, - // }; - - // draggableId = Object.keys(draggables)[0]; - - // destinationDroppable = getDroppableDimension({ - // id: droppableId, - // direction: 'horizontal', - // clientRect: getClientRect({ - // top: 0, - // left: 0, - // bottom: 100, - // right: 500, - // }), - // }); + const droppable = createDroppable({ + direction: 'horizontal', + droppableId: 'drop-1', + droppableRect: { top: 0, left: 0, bottom: 100, right: 500 }, + draggableRects: [ + { top: 0, left: 0, bottom: 100, right: 100 }, + { top: 0, left: 101, bottom: 100, right: 300 }, + { top: 0, left: 301, bottom: 100, right: 500 }, + ], }); it('should return to the total scroll diff if nothing has moved', () => { @@ -611,10 +525,7 @@ describe('get new home client offset', () => { const movement: DragMovement = { draggables: droppable.draggableIds.slice(1), - amount: { - x: droppable.draggableDimensions[0].page.withMargin.width, - y: 0, - }, + amount: origin, isBeyondStartPosition: true, }; @@ -652,10 +563,7 @@ describe('get new home client offset', () => { ); const movement: DragMovement = { draggables: droppable.draggableIds.slice(1), - amount: { - x: droppable.draggableDimensions[0].page.withMargin.width, - y: 0, - }, + amount: origin, isBeyondStartPosition: true, }; // this is where it needs to end up @@ -692,10 +600,7 @@ describe('get new home client offset', () => { const windowScrollDiff = pageOffset; const movement: DragMovement = { draggables: droppable.draggableIds.slice(1), - amount: { - x: droppable.draggableDimensions[0].page.withMargin.width, - y: 0, - }, + amount: origin, isBeyondStartPosition: true, }; // this is where it needs to end up @@ -748,10 +653,7 @@ describe('get new home client offset', () => { const movement: DragMovement = { draggables: droppable.draggableIds.slice(0, 2), - amount: { - x: droppable.draggableDimensions[2].page.withMargin.width, - y: 0, - }, + amount: origin, isBeyondStartPosition: false, }; @@ -790,10 +692,7 @@ describe('get new home client offset', () => { ); const movement: DragMovement = { draggables: droppable.draggableIds.slice(0, 2), - amount: { - x: droppable.draggableDimensions[2].page.withMargin.width, - y: 0, - }, + amount: origin, isBeyondStartPosition: false, }; // How much distance the item needs to travel to be in its new home @@ -830,10 +729,7 @@ describe('get new home client offset', () => { ); const movement: DragMovement = { draggables: droppable.draggableIds.slice(0, 2), - amount: { - x: droppable.draggableDimensions[2].page.withMargin.width, - y: 0, - }, + amount: origin, isBeyondStartPosition: false, }; // this is where it needs to end up @@ -860,4 +756,233 @@ describe('get new home client offset', () => { }); }); }); + + describe('multiple lists - vertical', () => { + const homeDroppable = createDroppable({ + droppableId: 'drop-home', + droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, + draggableRects: [ + { top: 0, left: 0, bottom: 100, right: 100 }, + { top: 101, left: 0, bottom: 300, right: 100 }, + { top: 301, left: 0, bottom: 600, right: 100 }, + ], + }); + + const destinationDroppable = createDroppable({ + droppableId: 'drop-destination', + droppableRect: { top: 100, left: 110, bottom: 700, right: 210 }, + draggableRects: [ + { top: 100, left: 110, bottom: 400, right: 210 }, + { top: 401, left: 110, bottom: 600, right: 210 }, + { top: 601, left: 110, bottom: 700, right: 210 }, + ], + }); + + const emptyDroppable = createDroppable({ + droppableId: 'drop-empty', + droppableRect: { top: 200, left: 220, bottom: 800, right: 320 }, + draggableRects: [], + }); + + const draggable = homeDroppable.draggableDimensions[0]; + + const allDraggables = { ...homeDroppable.draggables, ...destinationDroppable.draggables }; + + const selection = { + x: emptyDroppable.droppable.client.withMargin.left + 1, + y: emptyDroppable.droppable.client.withMargin.bottom + 1, + }; + + const clientOffset = subtract(selection, draggable.client.withMargin.center); + + const pageOffset = clientOffset; + + it('should move to the top of the droppable when landing in an empty droppable', () => { + const movement: DragMovement = { + draggables: emptyDroppable.draggableIds, + amount: origin, + isBeyondStartPosition: false, + }; + + // Align top-left of incoming draggable with top-left of droppable + const expected = { + x: ( + emptyDroppable.droppable.client.withMargin.left - + draggable.client.withMargin.left + ), + y: ( + emptyDroppable.droppable.client.withMargin.top - + draggable.client.withMargin.top + ), + }; + + const newHomeOffset = getNewHomeClientOffset({ + movement, + clientOffset, + pageOffset, + droppableScrollDiff: origin, + windowScrollDiff: origin, + draggables: allDraggables, + draggableId: draggable.id, + destinationDroppable: emptyDroppable.droppable, + }); + + expect(newHomeOffset).toEqual(expected); + }); + + it('should move into the top of the first-most displaced draggable when landing in a new droppable which already contains draggables', () => { + const movement: DragMovement = { + draggables: destinationDroppable.draggableIds.slice(2), + amount: origin, + isBeyondStartPosition: false, + }; + + const displacedDraggable = destinationDroppable.draggableDimensions[2]; + + // Align top-left of incoming draggable with top-left of displaced draggable + const expected = { + x: ( + displacedDraggable.client.withMargin.left - + draggable.client.withMargin.left + ), + y: ( + displacedDraggable.client.withMargin.top - + draggable.client.withMargin.top + ), + }; + + const newHomeOffset = getNewHomeClientOffset({ + movement, + clientOffset, + pageOffset, + droppableScrollDiff: origin, + windowScrollDiff: origin, + draggables: allDraggables, + draggableId: draggable.id, + destinationDroppable: destinationDroppable.droppable, + }); + + expect(newHomeOffset).toEqual(expected); + }); + + it('should move in after the last draggable when landing on the end of a new droppable which already contains draggables', () => { + const movement: DragMovement = { + draggables: [], + amount: origin, + isBeyondStartPosition: false, + }; + + const lastDraggable = destinationDroppable.draggableDimensions[2]; + + // Align top-left of incoming draggable with bottom-left of last draggable + const expected = { + x: ( + lastDraggable.client.withMargin.left - + draggable.client.withMargin.left + ), + y: ( + lastDraggable.client.withMargin.bottom - + draggable.client.withMargin.top + ), + }; + + const newHomeOffset = getNewHomeClientOffset({ + movement, + clientOffset, + pageOffset, + droppableScrollDiff: origin, + windowScrollDiff: origin, + draggables: allDraggables, + draggableId: draggable.id, + destinationDroppable: destinationDroppable.droppable, + }); + + expect(newHomeOffset).toEqual(expected); + }); + + it('should account for internal scrolling in the destination droppable', () => { + const movement: DragMovement = { + draggables: destinationDroppable.draggableIds.slice(2), + amount: origin, + isBeyondStartPosition: false, + }; + + const displacedDraggable = destinationDroppable.draggableDimensions[2]; + + const horizontalScrollAmount = 50; + const verticalScrollAmount = 100; + + const droppableScrollDiff = { + x: -horizontalScrollAmount, + y: -verticalScrollAmount, + }; + + // Offset alignment by scroll amount + const expected = { + x: ( + displacedDraggable.client.withMargin.left - + draggable.client.withMargin.left - + horizontalScrollAmount + ), + y: ( + displacedDraggable.client.withMargin.top - + draggable.client.withMargin.top - + verticalScrollAmount + ), + }; + + const newHomeOffset = getNewHomeClientOffset({ + movement, + clientOffset, + pageOffset, + droppableScrollDiff, + windowScrollDiff: origin, + draggables: allDraggables, + draggableId: draggable.id, + destinationDroppable: destinationDroppable.droppable, + }); + + expect(newHomeOffset).toEqual(expected); + }); + + it('should account for full page scrolling during the drag', () => { + const movement: DragMovement = { + draggables: destinationDroppable.draggableIds.slice(2), + amount: origin, + isBeyondStartPosition: false, + }; + + const displacedDraggable = destinationDroppable.draggableDimensions[2]; + + const windowScrollDiff = { + x: -100, + y: -200, + }; + + // Since the whole page scrolled there should be no net difference in alignment + const expected = { + x: ( + displacedDraggable.client.withMargin.left - + draggable.client.withMargin.left + ), + y: ( + displacedDraggable.client.withMargin.top - + draggable.client.withMargin.top + ), + }; + + const newHomeOffset = getNewHomeClientOffset({ + movement, + clientOffset, + pageOffset, + droppableScrollDiff: origin, + windowScrollDiff, + draggables: allDraggables, + draggableId: draggable.id, + destinationDroppable: destinationDroppable.droppable, + }); + + expect(newHomeOffset).toEqual(expected); + }); + }); }); From dc485a3c74c961be12bc9007b00485a911abcc7f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 4 Sep 2017 14:53:40 +1000 Subject: [PATCH 034/117] wip --- src/state/jump-to-next-index.js | 68 ++++++++++++++------------------- src/state/reducer.js | 1 + 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index ad90a16541..301d86c40c 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -5,6 +5,7 @@ import { add, patch, subtract, + absolute, } from './position'; import moveToEdge from './move-to-edge'; import type { @@ -58,6 +59,7 @@ const shift = (adjust: (original: Position, modification: Position) => Position) export default ({ isMovingForward, draggableId, + center, impact, draggables, droppables, @@ -102,47 +104,35 @@ export default ({ // if moving forward: move start edge of source to end edge of destination // if moving backward: move end edge of source to start edge of destination - const center = moveToEdge({ - source: draggable.page.withoutMargin, - sourceEdge: isMovingForward ? 'start' : 'end', - destination: destination.page.withMargin, - destinationEdge: isMovingForward ? 'start' : 'end', - destinationAxis: droppable.axis, - }); - const isMovingTowardStart = (isMovingForward && proposedIndex <= startIndex) || (!isMovingForward && proposedIndex >= startIndex); - const sizeDiff: Position = patch( - axis.line, - destination.page.withMargin[axis.size] - draggable.page.withMargin[axis.size] - ); - - const shifted = add(center, sizeDiff); - - // const shifted = (() => { - // const fragment: DimensionFragment = isMovingTowardStart ? - // atCurrentIndex.page.withMargin : - // draggable.page.withMargin; - - // const amount: Position = patch( - // axis.line, - // fragment[axis.size], - // ); - - // if (isMovingForward) { - // return subtract(center, amount); - // } - // return add(center, amount); - // })(); - - // const distance: number = isMovingTowardStart ? - // atCurrentIndex.page.withMargin[axis.size] : - // destination.page.withMargin[axis.size]; - - // const signed: number = isMovingForward ? distance : -distance; - - // const diff: Position = patch(axis.line, signed); + const newCenter: Position = (() => { + // If moving toward start, just add / remove the size of the dragging item + // Things have moved out of the way by the size of the dragging item - we are + // just undoing the movement + if (isMovingTowardStart) { + const size = patch(axis.line, destination.page.withMargin[axis.size]); + + return isMovingForward ? add(center, size) : subtract(center, size); + } + + // if moving away from the start - move to the start edge of the next draggable + const goal: Position = moveToEdge({ + source: draggable.page.withoutMargin, + sourceEdge: 'start', + destination: destination.page.withMargin, + destinationEdge: 'start', + destinationAxis: droppable.axis, + }); + + const sizeDiff: Position = patch( + axis.line, + draggable.page.withMargin[axis.size] - destination.page.withMargin[axis.size] + ); + + return isMovingForward ? subtract(goal, sizeDiff) : goal; + })(); // Calculate DragImpact @@ -169,7 +159,7 @@ export default ({ }; const result: JumpToNextResult = { - center: shifted, + center: newCenter, impact: newImpact, }; diff --git a/src/state/reducer.js b/src/state/reducer.js index d871e48571..459d326ff9 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -403,6 +403,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { isMovingForward, draggableId: existing.current.id, impact: existing.impact, + center: existing.current.page.center, draggables: state.dimension.draggable, droppables: state.dimension.droppable, }); From 18f16224c3ab7012545c8c04335b9fec237636d9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 4 Sep 2017 15:16:01 +1000 Subject: [PATCH 035/117] dragging with a keyboard now uses edges --- src/state/jump-to-next-index.js | 54 ++++++++------------------------- src/state/reducer.js | 1 - 2 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index 301d86c40c..574f8eb672 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -30,8 +30,6 @@ const getIndex = memoizeOne( type JumpToNextArgs = {| isMovingForward: boolean, draggableId: DraggableId, - // the current center position - center: Position, impact: DragImpact, draggables: DraggableDimensionMap, droppables: DroppableDimensionMap, @@ -41,16 +39,6 @@ export type JumpToNextResult = {| center: Position, impact: DragImpact, |} - -type ShiftPosition = (point: Position, size: number, axis: Axis) => Position; - -const shift = (adjust: (original: Position, modification: Position) => Position): ShiftPosition => - (point: Position, size: number, axis: Axis): Position => { - const amount: Position = patch(axis.line, size); - - return adjust(point, amount); - }; - // const pull = // const pull: ShiftPosition = shift(subtract, size: number); @@ -59,7 +47,6 @@ const shift = (adjust: (original: Position, modification: Position) => Position) export default ({ isMovingForward, draggableId, - center, impact, draggables, droppables, @@ -99,41 +86,24 @@ export default ({ } const destination: DraggableDimension = insideDroppable[proposedIndex]; - const atCurrentIndex: DraggableDimension = insideDroppable[currentIndex]; - - // if moving forward: move start edge of source to end edge of destination - // if moving backward: move end edge of source to start edge of destination - const isMovingTowardStart = (isMovingForward && proposedIndex <= startIndex) || (!isMovingForward && proposedIndex >= startIndex); - const newCenter: Position = (() => { - // If moving toward start, just add / remove the size of the dragging item - // Things have moved out of the way by the size of the dragging item - we are - // just undoing the movement - if (isMovingTowardStart) { - const size = patch(axis.line, destination.page.withMargin[axis.size]); - - return isMovingForward ? add(center, size) : subtract(center, size); + const edge = (() => { + if (!isMovingTowardStart) { + return isMovingForward ? 'end' : 'start'; } - - // if moving away from the start - move to the start edge of the next draggable - const goal: Position = moveToEdge({ - source: draggable.page.withoutMargin, - sourceEdge: 'start', - destination: destination.page.withMargin, - destinationEdge: 'start', - destinationAxis: droppable.axis, - }); - - const sizeDiff: Position = patch( - axis.line, - draggable.page.withMargin[axis.size] - destination.page.withMargin[axis.size] - ); - - return isMovingForward ? subtract(goal, sizeDiff) : goal; + return isMovingForward ? 'start' : 'end'; })(); + const newCenter = moveToEdge({ + source: draggable.page.withoutMargin, + sourceEdge: edge, + destination: destination.page.withoutMargin, + destinationEdge: edge, + destinationAxis: droppable.axis, + }); + // Calculate DragImpact // 1. If moving back towards where we started diff --git a/src/state/reducer.js b/src/state/reducer.js index 459d326ff9..d871e48571 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -403,7 +403,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { isMovingForward, draggableId: existing.current.id, impact: existing.impact, - center: existing.current.page.center, draggables: state.dimension.draggable, droppables: state.dimension.droppable, }); From d267a78bbba35787073ac31118b92f93ea15178b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 4 Sep 2017 18:51:28 +1000 Subject: [PATCH 036/117] minor enchancement to hooks integration test --- test/unit/integration/hooks-integration.spec.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/unit/integration/hooks-integration.spec.js b/test/unit/integration/hooks-integration.spec.js index da049eae58..b16263becb 100644 --- a/test/unit/integration/hooks-integration.spec.js +++ b/test/unit/integration/hooks-integration.spec.js @@ -61,13 +61,16 @@ describe('hooks integration', () => {

Droppable

{(draggableProvided: DraggableProvided) => ( -
-

Draggable

+
+
+

Draggable

+
+ {draggableProvided.placeholder}
)} From e8f5cd873e4cb2de566e0428d02f0bf74e70847d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 4 Sep 2017 22:13:02 +1000 Subject: [PATCH 037/117] fixing jump to next index tests --- src/state/jump-to-next-index.js | 12 +- test/unit/state/jump-to-next-index.spec.js | 261 +++++++++++---------- 2 files changed, 145 insertions(+), 128 deletions(-) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index 574f8eb672..430d724a15 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -1,13 +1,9 @@ // @flow import memoizeOne from 'memoize-one'; import getDraggablesInsideDroppable from './get-draggables-inside-droppable'; -import { - add, - patch, - subtract, - absolute, -} from './position'; +import { patch } from './position'; import moveToEdge from './move-to-edge'; +import type { Edge } from './move-to-edge'; import type { DraggableLocation, DraggableDimension, @@ -89,14 +85,14 @@ export default ({ const isMovingTowardStart = (isMovingForward && proposedIndex <= startIndex) || (!isMovingForward && proposedIndex >= startIndex); - const edge = (() => { + const edge: Edge = (() => { if (!isMovingTowardStart) { return isMovingForward ? 'end' : 'start'; } return isMovingForward ? 'start' : 'end'; })(); - const newCenter = moveToEdge({ + const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, sourceEdge: edge, destination: destination.page.withoutMargin, diff --git a/test/unit/state/jump-to-next-index.spec.js b/test/unit/state/jump-to-next-index.spec.js index 08343d29c1..38c36a5df6 100644 --- a/test/unit/state/jump-to-next-index.spec.js +++ b/test/unit/state/jump-to-next-index.spec.js @@ -3,6 +3,7 @@ import jumpToNextIndex from '../../../src/state/jump-to-next-index'; import type { JumpToNextResult } from '../../../src/state/jump-to-next-index'; import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; import getClientRect from '../../utils/get-client-rect'; +import moveToEdge from '../../../src/state/move-to-edge'; import { patch } from '../../../src/state/position'; import { vertical, horizontal } from '../../../src/state/axis'; import type { @@ -105,81 +106,84 @@ describe('jump to next index', () => { expect(result).toBe(null); }); - describe('is moving toward start position', () => { - describe('dragging item forward to starting position', () => { - // dragging the second item (draggable2), which has previously - // been moved backwards and is now in the first position + describe('is moving away from start position', () => { + describe('dragging first item forward one position', () => { + // dragging the first item forward into the second position + const movement: DragMovement = { + draggables: [], + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }; + const destination: DraggableLocation = { + index: 0, + droppableId: droppable.id, + }; const impact: DragImpact = { - movement: { - draggables: [draggable1.id], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, + movement, direction: axis.direction, - destination: { - index: 0, - droppableId: droppable.id, - }, + destination, }; const result: ?JumpToNextResult = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable2.id, + draggableId: draggable1.id, impact, draggables, droppables, }); if (!result) { - throw new Error('invalid result of jumpToNextIndex'); + throw new Error('invalid result'); } - it('should return the size of the dimension in the current index as the diff', () => { - const size: Position = patch( - axis.line, - draggable1.page.withMargin[axis.size] - ); + it('should move the end of the dragging item to the end of the next item', () => { + const expected: Position = moveToEdge({ + source: draggable1.page.withoutMargin, + sourceEdge: 'end', + destination: draggable2.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); - expect(result.diff).toEqual(size); + expect(result.center).toEqual(expected); }); - it('should return an empty impact', () => { + it('should move the item into the second spot and move the second item out of the way', () => { const expected: DragImpact = { movement: { - draggables: [], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: false, + draggables: [draggable2.id], + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: true, }, + direction: axis.direction, + // is now in the second position destination: { droppableId: droppable.id, index: 1, }, - direction: axis.direction, }; expect(result.impact).toEqual(expected); }); }); - describe('dragging forwards, but not beyond the starting position', () => { - // draggable3 has moved backwards past draggable2 and draggable1 + describe('dragging second item forward one position', () => { + const movement: DragMovement = { + draggables: [], + amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }; + const destination: DraggableLocation = { + index: 1, + droppableId: droppable.id, + }; const impact: DragImpact = { - movement: { - // second and first item have already moved - draggables: [draggable2.id, draggable1.id], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), - isBeyondStartPosition: true, - }, + movement, direction: axis.direction, - // draggable3 is now in the first position - destination: { - droppableId: droppable.id, - index: 0, - }, + destination, }; - // moving draggable3 forward one position const result: ?JumpToNextResult = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable3.id, + draggableId: draggable2.id, impact, draggables, droppables, @@ -189,51 +193,51 @@ describe('jump to next index', () => { throw new Error('invalid result'); } - it('should return the size of the moving dimension (draggable1)', () => { - const expected: Position = patch( - axis.line, - draggable1.page.withMargin[axis.size], - ); - expect(result.diff).toEqual(expected); + it('should move the end of the dragging item to the end of the next item', () => { + const expected: Position = moveToEdge({ + source: draggable2.page.withoutMargin, + sourceEdge: 'end', + destination: draggable3.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + + expect(result.center).toEqual(expected); }); - it('should remove the first dimension from the impact', () => { + it('should move the dragging item into the third spot and move the third item out of the way', () => { const expected: DragImpact = { movement: { - draggables: [draggable2.id], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), - // is still behind where it started - isBeyondStartPosition: false, + draggables: [draggable3.id], + amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + isBeyondStartPosition: true, }, direction: axis.direction, // is now in the second position destination: { droppableId: droppable.id, - index: 1, + index: 2, }, }; expect(result.impact).toEqual(expected); }); }); - }); - describe('is moving away from start position', () => { - describe('dragging first item forward one position', () => { - // dragging the first item forward into the second position - const movement: DragMovement = { - draggables: [], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }; - const destination: DraggableLocation = { - index: 0, - droppableId: droppable.id, - }; + describe('dragging first item forward one position after already moving it forward once', () => { const impact: DragImpact = { - movement, + movement: { + // second item has already moved + draggables: [draggable2.id], + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: true, + }, direction: axis.direction, - destination, + // draggable1 is now in the second position + destination: { + droppableId: droppable.id, + index: 1, + }, }; const result: ?JumpToNextResult = jumpToNextIndex({ isMovingForward: true, @@ -247,16 +251,24 @@ describe('jump to next index', () => { throw new Error('invalid result'); } - it('should return the size of the second dimension as the diff', () => { - const expected: Position = patch(axis.line, draggable2.page.withMargin[axis.size]); - - expect(result.diff).toEqual(expected); + it('should move the end of the dragging item to the end of the next item', () => { + // next dimension from the current index is draggable3 + const expected: Position = moveToEdge({ + source: draggable1.page.withoutMargin, + sourceEdge: 'end', + destination: draggable3.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + + expect(result.center).toEqual(expected); }); - it('should move the item into the second spot and move the second item out of the way', () => { + it('should move the third item out of the way', () => { const expected: DragImpact = { movement: { - draggables: [draggable2.id], + // adding draggable3 to the list + draggables: [draggable2.id, draggable3.id], amount: patch(axis.line, draggable1.page.withMargin[axis.size]), isBeyondStartPosition: true, }, @@ -264,28 +276,30 @@ describe('jump to next index', () => { // is now in the second position destination: { droppableId: droppable.id, - index: 1, + index: 2, }, }; expect(result.impact).toEqual(expected); }); }); + }); - describe('dragging second item forward one position', () => { - const movement: DragMovement = { - draggables: [], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }; - const destination: DraggableLocation = { - index: 1, - droppableId: droppable.id, - }; + describe('is moving toward start position', () => { + describe('dragging item forward to starting position', () => { + // dragging the second item (draggable2), which has previously + // been moved backwards and is now in the first position const impact: DragImpact = { - movement, + movement: { + draggables: [draggable1.id], + amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, direction: axis.direction, - destination, + destination: { + index: 0, + droppableId: droppable.id, + }, }; const result: ?JumpToNextResult = jumpToNextIndex({ isMovingForward: true, @@ -296,54 +310,59 @@ describe('jump to next index', () => { }); if (!result) { - throw new Error('invalid result'); + throw new Error('invalid result of jumpToNextIndex'); } - it('should return the size of the third dimension as the diff', () => { - const expected: Position = patch( - axis.line, - draggable3.page.withMargin[axis.size], - ); - expect(result.diff).toEqual(expected); + it('should move the start of the dragging item to the end of the previous item (which is itself)', () => { + const expected: Position = moveToEdge({ + source: draggable2.page.withoutMargin, + sourceEdge: 'start', + destination: draggable2.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); + + expect(result.center).toEqual(expected); }); - it('should move the dragging item into the third spot and move the third item out of the way', () => { + it('should return an empty impact', () => { const expected: DragImpact = { movement: { - draggables: [draggable3.id], + draggables: [], amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: true, + isBeyondStartPosition: false, }, - direction: axis.direction, - // is now in the second position destination: { droppableId: droppable.id, - index: 2, + index: 1, }, + direction: axis.direction, }; expect(result.impact).toEqual(expected); }); }); - describe('dragging first item forward one position after already moving it forward once', () => { + describe('dragging forwards, but not beyond the starting position', () => { + // draggable3 has moved backwards past draggable2 and draggable1 const impact: DragImpact = { movement: { - // second item has already moved - draggables: [draggable2.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + // second and first item have already moved + draggables: [draggable2.id, draggable1.id], + amount: patch(axis.line, draggable3.page.withMargin[axis.size]), isBeyondStartPosition: true, }, direction: axis.direction, - // draggable1 is now in the second position + // draggable3 is now in the first position destination: { droppableId: droppable.id, - index: 1, + index: 0, }, }; + // moving draggable3 forward one position const result: ?JumpToNextResult = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable1.id, + draggableId: draggable3.id, impact, draggables, droppables, @@ -353,29 +372,31 @@ describe('jump to next index', () => { throw new Error('invalid result'); } - it('should return the size of the third dimension', () => { - // next dimension from the current index is draggable3 - const expected: Position = patch( - axis.line, - draggable3.page.withMargin[axis.size], - ); + it('should move to the start of the draggable item to the start position of the destination draggable', () => { + const expected: Position = moveToEdge({ + source: draggable3.page.withoutMargin, + sourceEdge: 'start', + destination: draggable2.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); - expect(result.diff).toEqual(expected); + expect(result.center).toEqual(expected); }); - it('should move the third item out of the way', () => { + it('should remove the first dimension from the impact', () => { const expected: DragImpact = { movement: { - // adding draggable3 to the list - draggables: [draggable2.id, draggable3.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: true, + draggables: [draggable2.id], + amount: patch(axis.line, draggable3.page.withMargin[axis.size]), + // is still behind where it started + isBeyondStartPosition: false, }, direction: axis.direction, // is now in the second position destination: { droppableId: droppable.id, - index: 2, + index: 1, }, }; From 95c96ca62f4b12e40282a5bd6816f44caa46f74f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 5 Sep 2017 08:07:12 +1000 Subject: [PATCH 038/117] fixing remaining jump-to-next-index tests --- src/state/jump-to-next-index.js | 2 + test/unit/state/jump-to-next-index.spec.js | 163 +++++++++++---------- 2 files changed, 91 insertions(+), 74 deletions(-) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index 430d724a15..b190dad5f1 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -86,9 +86,11 @@ export default ({ (!isMovingForward && proposedIndex >= startIndex); const edge: Edge = (() => { + // is moving away from the start if (!isMovingTowardStart) { return isMovingForward ? 'end' : 'start'; } + // is moving back towards the start return isMovingForward ? 'start' : 'end'; })(); diff --git a/test/unit/state/jump-to-next-index.spec.js b/test/unit/state/jump-to-next-index.spec.js index 38c36a5df6..d49f0120e5 100644 --- a/test/unit/state/jump-to-next-index.spec.js +++ b/test/unit/state/jump-to-next-index.spec.js @@ -313,7 +313,7 @@ describe('jump to next index', () => { throw new Error('invalid result of jumpToNextIndex'); } - it('should move the start of the dragging item to the end of the previous item (which is itself)', () => { + it('should move the start of the dragging item to the end of the previous item (which its original position)', () => { const expected: Position = moveToEdge({ source: draggable2.page.withoutMargin, sourceEdge: 'start', @@ -323,6 +323,8 @@ describe('jump to next index', () => { }); expect(result.center).toEqual(expected); + // is now back at its original position + expect(result.center).toEqual(draggable2.page.withoutMargin.center); }); it('should return an empty impact', () => { @@ -432,21 +434,20 @@ describe('jump to next index', () => { expect(result).toBe(null); }); - describe('is moving towards the start position', () => { - describe('moving back to original position', () => { - // dragged the second item (draggable2) forward once, and is now - // moving backwards towards the start again + describe('is moving away from start position', () => { + describe('dragging the second item back to the first position', () => { + // no impact yet const impact: DragImpact = { movement: { - draggables: [draggable3.id], + draggables: [], amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: true, + isBeyondStartPosition: false, }, - direction: axis.direction, destination: { - index: 2, droppableId: droppable.id, + index: 1, }, + direction: axis.direction, }; const result: ?JumpToNextResult = jumpToNextIndex({ isMovingForward: false, @@ -460,26 +461,29 @@ describe('jump to next index', () => { throw new Error('invalid result'); } - it('should return the size of the dimension that will move back to its home', () => { - const expected: Position = patch( - axis.line, - // amount is negative because we are moving backwards - -draggable3.page.withMargin[axis.size], - ); + it('should move the start of the draggable to the start of the previous draggable', () => { + const expected: Position = moveToEdge({ + source: draggable2.page.withoutMargin, + sourceEdge: 'start', + destination: draggable1.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); - expect(result.diff).toEqual(expected); + expect(result.center).toEqual(expected); }); - it('should return an empty impact', () => { + it('should add the first draggable to the drag impact', () => { const expected: DragImpact = { movement: { - draggables: [], + draggables: [draggable1.id], amount: patch(axis.line, draggable2.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { droppableId: droppable.id, - index: 1, + // is now in the first position + index: 0, }, direction: axis.direction, }; @@ -488,25 +492,22 @@ describe('jump to next index', () => { }); }); - describe('moving back, but not far enough to be at the start yet', () => { - // dragged the first item: - // forward twice so it is in the third position - // then moving backward so it is in the second position + describe('dragging the third item back to the second position', () => { const impact: DragImpact = { movement: { - draggables: [draggable2.id, draggable3.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: true, + draggables: [], + amount: patch(axis.line, draggable3.page.withMargin[axis.size]), + isBeyondStartPosition: false, }, - direction: axis.direction, destination: { - index: 2, droppableId: droppable.id, + index: 2, }, + direction: axis.direction, }; const result: ?JumpToNextResult = jumpToNextIndex({ isMovingForward: false, - draggableId: draggable1.id, + draggableId: draggable3.id, impact, draggables, droppables, @@ -516,26 +517,28 @@ describe('jump to next index', () => { throw new Error('invalid result'); } - it('should return the size of the dimension that will move back to its home', () => { - const expected: Position = patch( - axis.line, - // amount is negative because we are moving backwards - -draggable3.page.withMargin[axis.size], - ); + it('should move the start of the draggable to the start of the previous draggable', () => { + const expected: Position = moveToEdge({ + source: draggable3.page.withoutMargin, + sourceEdge: 'start', + destination: draggable2.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); - expect(result.diff).toEqual(expected); + expect(result.center).toEqual(expected); }); - it('should remove the third draggable from the drag impact', () => { + it('should add the second draggable to the drag impact', () => { const expected: DragImpact = { movement: { - // draggable3 has been removed draggables: [draggable2.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: true, + amount: patch(axis.line, draggable3.page.withMargin[axis.size]), + isBeyondStartPosition: false, }, destination: { droppableId: droppable.id, + // is now in the second position index: 1, }, direction: axis.direction, @@ -546,20 +549,21 @@ describe('jump to next index', () => { }); }); - describe('is moving away from start position', () => { - describe('dragging the second item back to the first position', () => { - // no impact yet + describe('is moving towards the start position', () => { + describe('moving back to original position', () => { + // dragged the second item (draggable2) forward once, and is now + // moving backwards towards the start again const impact: DragImpact = { movement: { - draggables: [], + draggables: [draggable3.id], amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: false, + isBeyondStartPosition: true, }, + direction: axis.direction, destination: { + index: 2, droppableId: droppable.id, - index: 1, }, - direction: axis.direction, }; const result: ?JumpToNextResult = jumpToNextIndex({ isMovingForward: false, @@ -573,26 +577,31 @@ describe('jump to next index', () => { throw new Error('invalid result'); } - it('should return negated size of draggable1 as the diff', () => { - const expected: Position = patch( - axis.line, - -draggable1.page.withMargin[axis.size], - ); + it('should move the end of the draggable to the end of the next draggable (which is its original position)', () => { + const expected: Position = moveToEdge({ + source: draggable2.page.withoutMargin, + sourceEdge: 'end', + // destination is itself as moving back to home + destination: draggable2.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); - expect(result.diff).toEqual(expected); + expect(result.center).toEqual(expected); + // moved back to its original position + expect(result.center).toEqual(draggable2.page.withoutMargin.center); }); - it('should add the first draggable to the drag impact', () => { + it('should return an empty impact', () => { const expected: DragImpact = { movement: { - draggables: [draggable1.id], + draggables: [], amount: patch(axis.line, draggable2.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { droppableId: droppable.id, - // is now in the first position - index: 0, + index: 1, }, direction: axis.direction, }; @@ -601,22 +610,25 @@ describe('jump to next index', () => { }); }); - describe('dragging the third item back to the second position', () => { + describe('moving back, but not far enough to be at the start yet', () => { + // dragged the first item: + // forward twice so it is in the third position + // then moving backward so it is in the second position const impact: DragImpact = { movement: { - draggables: [], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), - isBeyondStartPosition: false, + draggables: [draggable2.id, draggable3.id], + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: true, }, + direction: axis.direction, destination: { - droppableId: droppable.id, index: 2, + droppableId: droppable.id, }, - direction: axis.direction, }; const result: ?JumpToNextResult = jumpToNextIndex({ isMovingForward: false, - draggableId: draggable3.id, + draggableId: draggable1.id, impact, draggables, droppables, @@ -626,25 +638,28 @@ describe('jump to next index', () => { throw new Error('invalid result'); } - it('should return the negated size of draggable2 as the diff', () => { - const expected: Position = patch( - axis.line, - -draggable2.page.withMargin[axis.size], - ); + it('should move the end of the draggable to the end of the previous draggable', () => { + const expected: Position = moveToEdge({ + source: draggable1.page.withoutMargin, + sourceEdge: 'end', + destination: draggable2.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); - expect(result.diff).toEqual(expected); + expect(result.center).toEqual(expected); }); - it('should add the second draggable to the drag impact', () => { + it('should remove the third draggable from the drag impact', () => { const expected: DragImpact = { movement: { + // draggable3 has been removed draggables: [draggable2.id], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), - isBeyondStartPosition: false, + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: true, }, destination: { droppableId: droppable.id, - // is now in the second position index: 1, }, direction: axis.direction, From b63cd2a0927112c8adb93d11ec4a0a73e39a5659 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 5 Sep 2017 14:22:05 +1000 Subject: [PATCH 039/117] spiking --- src/state/jump-to-next-index.js | 120 +++++++++++++++++- src/state/move-to-best-droppable/index.js | 13 +- .../move-to-new-spot.js | 120 ++++++++++++++++-- src/state/reducer.js | 5 +- src/types.js | 10 +- 5 files changed, 247 insertions(+), 21 deletions(-) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index b190dad5f1..fe4a6da33e 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -27,8 +27,8 @@ type JumpToNextArgs = {| isMovingForward: boolean, draggableId: DraggableId, impact: DragImpact, + droppable: DroppableDimension, draggables: DraggableDimensionMap, - droppables: DroppableDimensionMap, |} export type JumpToNextResult = {| @@ -44,8 +44,8 @@ export default ({ isMovingForward, draggableId, impact, + droppable, draggables, - droppables, }: JumpToNextArgs): ?JumpToNextResult => { if (!impact.destination) { console.error('cannot move forward when there is not previous destination'); @@ -53,7 +53,6 @@ export default ({ } const location: DraggableLocation = impact.destination; - const droppable: DroppableDimension = droppables[location.droppableId]; const draggable: DraggableDimension = draggables[draggableId]; const axis: Axis = droppable.axis; @@ -62,6 +61,121 @@ export default ({ draggables, ); + // has the dragging item moved into a new list? + const isInHomeList: boolean = draggable.droppableId === droppable.id; + + if (!isInHomeList) { + console.log('not in home list!'); + // if draggable is not in home list + const currentIndex: number = location.index; + const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; + const startIndex: number = impact.foreignDestinationStartIndex; + + // cannot move forward beyond the last item + if (proposedIndex > insideDroppable.length) { + return null; + } + + // cannot move before the first item + if (proposedIndex < 0) { + return null; + } + + const isMovingToEnd = proposedIndex === insideDroppable.length; + + console.log('isMovingToEnd', isMovingToEnd); + console.log('foreignDestinationStartIndex', startIndex); + + const destination: DraggableDimension = + insideDroppable[isMovingToEnd ? proposedIndex - 1 : proposedIndex]; + + // const sourceEdge = isMovingForward ? 'start' : 'end'; + + // const destinationEdge = (() => { + // if (isMovingToEnd) { + // return 'end'; + // } + + // return isMovingForward ? 'start' : 'end'; + // })(); + + const isMovingTowardStart = (isMovingForward && proposedIndex <= startIndex) || + (!isMovingForward && proposedIndex >= startIndex); + + const sourceEdge = (() => { + if (isMovingToEnd) { + return 'start'; + } + + if (!isMovingTowardStart) { + return isMovingForward ? 'start' : 'end'; + } + // is moving back towards the start + return isMovingForward ? 'start' : 'end'; + })(); + + const destinationEdge = (() => { + if (isMovingToEnd) { + return 'end'; + } + + if (!isMovingTowardStart) { + return isMovingForward ? 'start' : 'end'; + } + // is moving back towards the start + return isMovingForward ? 'start' : 'end'; + })(); + // console.log('edge', edge); + // console.log('is moving forward', isMovingForward); + // console.log('isMovingTowardStart', isMovingTowardStart); + + const newCenter: Position = moveToEdge({ + source: draggable.page.withoutMargin, + sourceEdge, + destination: isMovingToEnd ? destination.page.withMargin : destination.page.withoutMargin, + destinationEdge, + destinationAxis: droppable.axis, + }); + + const moved: DraggableId[] = (() => { + if (isMovingTowardStart) { + return [...impact.movement.draggables, destination.id]; + } + + // strip first item off the list + return impact.movement.draggables.slice(1, impact.movement.draggables.length); + })(); + + console.log('moved', moved); + + const newImpact: DragImpact = { + movement: { + draggables: moved, + // The amount of movement will always be the size of the dragging item + amount: patch(axis.line, draggable.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + destination: { + droppableId: droppable.id, + index: proposedIndex, + }, + direction: droppable.axis.direction, + foreignDestinationStartIndex: impact.foreignDestinationStartIndex, + }; + + // console.log('returning result', { newCenter, newImpact }); + + return { + center: newCenter, + impact: newImpact, + }; + } + + // even if not in home list - + + // If not in home list - need to insert draggable into correct position in list + // const tempIndex: number = impact.destination.index; + const startIndex: number = getIndex(insideDroppable, draggable); const currentIndex: number = location.index; const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index ce299c00b6..33a8e19c1e 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -12,6 +12,7 @@ import type { DraggableDimension, DraggableDimensionMap, DroppableDimensionMap, + DraggableLocation, } from '../../types'; type Args = {| @@ -22,6 +23,10 @@ type Args = {| draggableId: DraggableId, // the droppable the dragging item is in droppableId: DroppableId, + // the original location of the draggable + home: DraggableLocation, + // the current drag impact + impact: DragImpact, // all the dimensions in the system draggables: DraggableDimensionMap, droppables: DroppableDimensionMap, @@ -32,7 +37,9 @@ export default ({ center, draggableId, droppableId, + home, draggables, + impact, droppables, }: Args): ?Result => { const draggable: DraggableDimension = draggables[draggableId]; @@ -65,10 +72,12 @@ export default ({ return moveToNewSpot({ center, - source, - destination, draggable, target, + source, + destination, + home, + impact, draggables, }); }; diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index abb2963bf3..41ac71a275 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -1,13 +1,17 @@ // @flow import { subtract, patch } from '../position'; +import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import moveToEdge from '../move-to-edge'; +import jumpToNextIndex from '../jump-to-next-index'; import type { Axis, Position, DragImpact, + DraggableId, DraggableDimension, DroppableDimension, DraggableDimensionMap, + DraggableLocation, } from '../../types'; type Args = {| @@ -22,6 +26,10 @@ type Args = {| source: DroppableDimension, // the droppable the draggable is moving to destination: DroppableDimension, + // the source location of the draggable + home: DraggableLocation, + // the current drag impact + impact: DragImpact, // all the draggables in the system draggables: DraggableDimensionMap, |} @@ -39,8 +47,10 @@ export default ({ destination, draggable, target, + home, + impact, draggables, -}: Args): Result => { +}: Args): ?Result => { const destinationAxis: Axis = destination.axis; const sourceAxis: Axis = source.axis; const amount: Position = patch( @@ -63,7 +73,7 @@ export default ({ destinationAxis, }); - const impact: DragImpact = { + const newImpact: DragImpact = { movement: { draggables: [], amount, @@ -79,36 +89,117 @@ export default ({ return { center: newCenter, - impact, + impact: newImpact, }; } // 2. Moving to a populated droppable + const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable( + destination, draggables + ); + + const isGoingBeforeTarget: boolean = center[sourceAxis.line] < + target.page.withMargin.center[sourceAxis.line]; + + const targetIndex: number = insideDestination.indexOf(target); + const proposedIndex: number = isGoingBeforeTarget ? targetIndex : targetIndex + 1; + + if (targetIndex === -1) { + console.error('could not find target inside destination'); + return null; + } + + const isReturningToHomeList = destination.id === draggable.droppableId; + + if (isReturningToHomeList) { + console.log('returning to home list'); + console.log('proposed index', proposedIndex); + // returning to original position + if (targetIndex === home.index) { + console.log('returning to original position'); + const newCenter: Position = draggable.page.withoutMargin.center; + const newImpact: DragImpact = { + movement: { + draggables: [], + amount, + // TODO: not sure what this should be + isBeyondStartPosition: false, + }, + direction: destinationAxis.direction, + destination: { + droppableId: destination.id, + index: 0, + }, + }; + + return { + center: newCenter, + impact: newImpact, + }; + } + + // TODO: need to give an appropriate impact! + // TODO: broken if moving back to list when current list is impacted + + const result = jumpToNextIndex({ + isMovingForward: isGoingBeforeTarget, + draggableId: draggable.id, + impact, + draggables, + droppable: destination, + }); + + console.log('result moving back home', result); + + return result; + + // const newCenter: Position = moveToEdge({ + // source: draggable.page.withoutMargin, + // sourceEdge: 'start', + // destination: target.page.withMargin, + // destinationEdge: 'start', + // destinationAxis, + // }); + + // const impact: DragImpact = { + // draggables: [], + // amount, + // isBeyondStartPosition: proposedIndex > + // }; + + // return { + // center: newCenter, + // impact: noImpact, + // }; + } + // 1. If isGoingBefore: need to move draggable start edge to start edge of target // Then need to move the target and everything after it forward // 2. If is going after: need to move draggable start edge to the end of the target // Then need to move everything after the target forward - const isGoingBeforeTarget: boolean = center[sourceAxis.line] < - target.page.withMargin.center[sourceAxis.line]; + // const isGoingBeforeTarget: boolean = center[sourceAxis.line] < + // target.page.withMargin.center[sourceAxis.line]; + + // const proposedIndex: number = isGoingBeforeTarget ? targetIndex : targetIndex + 1; const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, - // TODO: source edge will always be start - unless moving to home column? sourceEdge: 'start', destination: target.page.withMargin, destinationEdge: isGoingBeforeTarget ? 'start' : 'end', destinationAxis, }); - // const targetIndex: number = getDraggablesInDroppable( - // target, dragg - // ); + // need to get the index of the draggable that we are moving relative to + + const needsToMove: DraggableId[] = insideDestination + .slice(proposedIndex, insideDestination.length) + .map((dimension: DraggableDimension): DraggableId => dimension.id); - // TODO - const impact: DragImpact = { + const newImpact: DragImpact = { movement: { - draggables: [], + draggables: needsToMove, amount, // TODO: not sure what this should be isBeyondStartPosition: false, @@ -116,12 +207,13 @@ export default ({ direction: destinationAxis.direction, destination: { droppableId: destination.id, - index: 0, + index: proposedIndex, }, + foreignDestinationStartIndex: proposedIndex, }; return { center: newCenter, - impact, + impact: newImpact, }; }; diff --git a/src/state/reducer.js b/src/state/reducer.js index d871e48571..cc285ead0e 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -403,8 +403,8 @@ export default (state: State = clean('IDLE'), action: Action): State => { isMovingForward, draggableId: existing.current.id, impact: existing.impact, + droppable: state.dimension.droppable[existing.impact.destination.droppableId], draggables: state.dimension.draggable, - droppables: state.dimension.droppable, }); // cannot move anyway (at the beginning or end of a list) @@ -459,12 +459,15 @@ export default (state: State = clean('IDLE'), action: Action): State => { const draggableId: DraggableId = current.id; const center: Position = current.page.center; const droppableId: DroppableId = state.drag.impact.destination.droppableId; + const home: DraggableLocation = state.drag.initial.source; const result: ?MoveToNewSpotResult = moveToBestDroppable({ isMovingForward: action.type === 'CROSS_AXIS_MOVE_FORWARD', center, draggableId, droppableId, + home, + impact: state.drag.impact, draggables: state.dimension.draggable, droppables: state.dimension.droppable, }); diff --git a/src/types.js b/src/types.js index 936cb26275..3faf5cdbd5 100644 --- a/src/types.js +++ b/src/types.js @@ -103,7 +103,11 @@ export type DragImpact = {| movement: DragMovement, // the direction of the Droppable you are over direction: ?Direction, - destination: ?DraggableLocation + destination: ?DraggableLocation, + // The starting index within a droppable + // This might be the original start index + // or it could be the first index received once jumped into a list + foreignDestinationStartIndex?: number, |} export type InitialDragLocation = {| @@ -150,7 +154,11 @@ export type CurrentDrag = {| windowScroll: Position, // viewport + scroll + droppable scroll withinDroppable: WithinDroppable, + // whether or not movements should be animated shouldAnimate: boolean, + droppable: DraggableLocation & {| + startIndex: number, + |} |} // published when a drag starts From 1631663485076d22eeae14938fbaab7d4fa442d3 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 5 Sep 2017 17:08:46 +1000 Subject: [PATCH 040/117] work in progress --- src/state/get-drag-impact.js | 2 + src/state/jump-to-next-index.js | 71 +++++++++++++++---- .../move-to-new-spot.js | 2 + src/state/reducer.js | 18 ++--- src/types.js | 2 + 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index f829dbad4f..3476866b9f 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -153,6 +153,8 @@ export default ({ isBeyondStartPosition: shouldDisplaceItemsForward, }; + console.log('mouse moved', moved); + const impact: DragImpact = { movement, direction: axis.direction, diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index fe4a6da33e..f70bdc4518 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -65,11 +65,15 @@ export default ({ const isInHomeList: boolean = draggable.droppableId === droppable.id; if (!isInHomeList) { + console.group('not in home list'); console.log('not in home list!'); // if draggable is not in home list const currentIndex: number = location.index; const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; const startIndex: number = impact.foreignDestinationStartIndex; + console.log('proposed index', proposedIndex); + console.log('currentIndex', currentIndex); + console.log('start index', startIndex); // cannot move forward beyond the last item if (proposedIndex > insideDroppable.length) { @@ -81,13 +85,24 @@ export default ({ return null; } - const isMovingToEnd = proposedIndex === insideDroppable.length; - - console.log('isMovingToEnd', isMovingToEnd); console.log('foreignDestinationStartIndex', startIndex); - const destination: DraggableDimension = - insideDroppable[isMovingToEnd ? proposedIndex - 1 : proposedIndex]; + const destinationIndex = proposedIndex >= startIndex ? proposedIndex - 1 : proposedIndex; + + console.log('destinationIndex', destinationIndex); + + // the index's need to be adjusted to take into account the additional element + // 'inserted' at start position + const destination: DimensionFragment = (() => { + if (proposedIndex === 0) { + console.log('using droppable as target'); + return insideDroppable[0].page.withMargin; + } + + console.log('destination id', insideDroppable[destinationIndex].id); + return insideDroppable[destinationIndex].page.withMargin; + })(); + const atProposedIndex: DraggableDimension = insideDroppable[proposedIndex]; // const sourceEdge = isMovingForward ? 'start' : 'end'; @@ -103,28 +118,33 @@ export default ({ (!isMovingForward && proposedIndex >= startIndex); const sourceEdge = (() => { - if (isMovingToEnd) { + if (proposedIndex === 0) { return 'start'; } + // is moving away from the start if (!isMovingTowardStart) { return isMovingForward ? 'start' : 'end'; } - // is moving back towards the start - return isMovingForward ? 'start' : 'end'; + // is moving back towards the start + return isMovingForward ? 'start' : 'start'; })(); const destinationEdge = (() => { - if (isMovingToEnd) { - return 'end'; + if (proposedIndex === 0) { + return 'start'; } + // is moving away from the start if (!isMovingTowardStart) { - return isMovingForward ? 'start' : 'end'; + return isMovingForward ? 'end' : 'start'; } - // is moving back towards the start + // is moving back towards the start return isMovingForward ? 'start' : 'end'; })(); + + console.log('sourceEdge:', sourceEdge); + console.log('destinationEdge:', destinationEdge); // console.log('edge', edge); // console.log('is moving forward', isMovingForward); // console.log('isMovingTowardStart', isMovingTowardStart); @@ -132,17 +152,33 @@ export default ({ const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, sourceEdge, - destination: isMovingToEnd ? destination.page.withMargin : destination.page.withoutMargin, + destination, destinationEdge, destinationAxis: droppable.axis, }); + console.log('is moving towards start', isMovingTowardStart); + console.log('is moving forward', isMovingForward); + const moved: DraggableId[] = (() => { if (isMovingTowardStart) { - return [...impact.movement.draggables, destination.id]; + // if moving backwards towards the start + if (!isMovingForward) { + // need to trim the existing movement + return [atProposedIndex.id, ...impact.movement.draggables]; + } + return impact.movement.draggables.slice(0, impact.movement.draggables.length - 1); } - // strip first item off the list + // if moving away from start and moving backwards + if (!isMovingForward) { + // need to add the destination to the impacted + return [...impact.movement.draggables, atProposedIndex.id]; + } + + // is moving away from the start and moving forward + // need to add the destination to the impacted + // when moving away we need to trim the impacted as it has already moved down to make room return impact.movement.draggables.slice(1, impact.movement.draggables.length); })(); @@ -165,6 +201,8 @@ export default ({ // console.log('returning result', { newCenter, newImpact }); + console.groupEnd(); + return { center: newCenter, impact: newImpact, @@ -226,6 +264,9 @@ export default ({ impact.movement.draggables.slice(0, impact.movement.draggables.length - 1) : [...impact.movement.draggables, destination.id]; + console.log('moved', moved); + console.log('destination', destination); + const newImpact: DragImpact = { movement: { draggables: moved, diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index 41ac71a275..7926ea6c44 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -197,6 +197,8 @@ export default ({ .slice(proposedIndex, insideDestination.length) .map((dimension: DraggableDimension): DraggableId => dimension.id); + console.log('cross axis movement', needsToMove); + const newImpact: DragImpact = { movement: { draggables: needsToMove, diff --git a/src/state/reducer.js b/src/state/reducer.js index cc285ead0e..6fe036ec38 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -418,15 +418,15 @@ export default (state: State = clean('IDLE'), action: Action): State => { // const client: Position = add(existing.current.client.selection, diff); // current limitation: cannot go beyond visible border of list - const droppableId: ?DroppableId = getDroppableOver( - result.center, state.dimension.droppable, - ); - - if (!droppableId) { - // eslint-disable-next-line no-console - console.info('currently not supporting moving a draggable outside the visibility bounds of a droppable'); - return state; - } + // const droppableId: ?DroppableId = getDroppableOver( + // result.center, state.dimension.droppable, + // ); + + // if (!droppableId) { + // // eslint-disable-next-line no-console + // console.info('currently not supporting moving a draggable outside the visibility bounds of a droppable'); + // return state; + // } return move({ state, diff --git a/src/types.js b/src/types.js index 3faf5cdbd5..3f97dcd6ef 100644 --- a/src/types.js +++ b/src/types.js @@ -93,6 +93,8 @@ export type DraggableDimensionMap = { [key: DraggableId]: DraggableDimension }; export type DroppableDimensionMap = { [key: DroppableId]: DroppableDimension }; export type DragMovement = {| + // The draggables that need to move in response to a drag. + // Ordered by the closest to the start of the droppable draggables: DraggableId[], amount: Position, // is moving forward relative to the starting position From 6d202896c0ce1de936a8a094629b5395c00c5004 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 5 Sep 2017 17:27:10 +1000 Subject: [PATCH 041/117] it works --- src/state/jump-to-next-index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index f70bdc4518..12366ad634 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -119,6 +119,7 @@ export default ({ const sourceEdge = (() => { if (proposedIndex === 0) { + console.log('returning custom edge'); return 'start'; } @@ -132,6 +133,7 @@ export default ({ const destinationEdge = (() => { if (proposedIndex === 0) { + console.log('returning custom edge'); return 'start'; } @@ -140,7 +142,7 @@ export default ({ return isMovingForward ? 'end' : 'start'; } // is moving back towards the start - return isMovingForward ? 'start' : 'end'; + return isMovingForward ? 'end' : 'end'; })(); console.log('sourceEdge:', sourceEdge); @@ -167,13 +169,13 @@ export default ({ // need to trim the existing movement return [atProposedIndex.id, ...impact.movement.draggables]; } - return impact.movement.draggables.slice(0, impact.movement.draggables.length - 1); + return impact.movement.draggables.slice(1, impact.movement.draggables.length); } // if moving away from start and moving backwards if (!isMovingForward) { // need to add the destination to the impacted - return [...impact.movement.draggables, atProposedIndex.id]; + return [atProposedIndex.id, ...impact.movement.draggables]; } // is moving away from the start and moving forward From 7b779aee2d9ce5e73ef4cd6bdc17ba883329baca Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 5 Sep 2017 20:27:44 +1000 Subject: [PATCH 042/117] wip --- src/state/jump-to-next-index.js | 10 +++++----- .../move-to-new-spot.js | 19 ------------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index 12366ad634..15c8b62e72 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -80,14 +80,14 @@ export default ({ return null; } - // cannot move before the first item + // cannot move before the first item if (proposedIndex < 0) { return null; } console.log('foreignDestinationStartIndex', startIndex); - const destinationIndex = proposedIndex >= startIndex ? proposedIndex - 1 : proposedIndex; + const destinationIndex = proposedIndex >= startIndex ? proposedIndex - 1 : proposedIndex - 1; console.log('destinationIndex', destinationIndex); @@ -95,7 +95,7 @@ export default ({ // 'inserted' at start position const destination: DimensionFragment = (() => { if (proposedIndex === 0) { - console.log('using droppable as target'); + console.log('destination id', insideDroppable[0].id); return insideDroppable[0].page.withMargin; } @@ -125,7 +125,7 @@ export default ({ // is moving away from the start if (!isMovingTowardStart) { - return isMovingForward ? 'start' : 'end'; + return isMovingForward ? 'start' : 'start'; } // is moving back towards the start return isMovingForward ? 'start' : 'start'; @@ -139,7 +139,7 @@ export default ({ // is moving away from the start if (!isMovingTowardStart) { - return isMovingForward ? 'end' : 'start'; + return isMovingForward ? 'end' : 'end'; } // is moving back towards the start return isMovingForward ? 'end' : 'end'; diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index 7926ea6c44..1c540f1e1b 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -153,25 +153,6 @@ export default ({ console.log('result moving back home', result); return result; - - // const newCenter: Position = moveToEdge({ - // source: draggable.page.withoutMargin, - // sourceEdge: 'start', - // destination: target.page.withMargin, - // destinationEdge: 'start', - // destinationAxis, - // }); - - // const impact: DragImpact = { - // draggables: [], - // amount, - // isBeyondStartPosition: proposedIndex > - // }; - - // return { - // center: newCenter, - // impact: noImpact, - // }; } // 1. If isGoingBefore: need to move draggable start edge to start edge of target From fd05533c929184dee47ed8148109749e7b94429b Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 08:41:06 +1000 Subject: [PATCH 043/117] logging --- src/state/jump-to-next-index.js | 2 ++ src/state/move-to-best-droppable/move-to-new-spot.js | 1 + 2 files changed, 3 insertions(+) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.js index 15c8b62e72..48f3c6b0e2 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.js @@ -216,6 +216,8 @@ export default ({ // If not in home list - need to insert draggable into correct position in list // const tempIndex: number = impact.destination.index; + console.log('moving when in home list') + const startIndex: number = getIndex(insideDroppable, draggable); const currentIndex: number = location.index; const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-spot.js index 1c540f1e1b..4a882d7f65 100644 --- a/src/state/move-to-best-droppable/move-to-new-spot.js +++ b/src/state/move-to-best-droppable/move-to-new-spot.js @@ -141,6 +141,7 @@ export default ({ // TODO: need to give an appropriate impact! // TODO: broken if moving back to list when current list is impacted + console.info('returning to original list - but not in original position'); const result = jumpToNextIndex({ isMovingForward: isGoingBeforeTarget, From 04c1ca81bcdbedf079b39141100b5dda29908f1a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 09:57:30 +1000 Subject: [PATCH 044/117] seperating jump to index functions for my sanity --- ...ext-index.js => jump-to-next-index.old.js} | 32 +---- .../jump-to-next-index/in-foreign-list.js | 91 ++++++++++++++ src/state/jump-to-next-index/in-home-list.js | 115 ++++++++++++++++++ src/state/jump-to-next-index/index.js | 18 +++ .../jump-to-next-index-types.js | 21 ++++ src/state/move-to-best-droppable/index.js | 6 +- ...o-new-spot.js => move-to-new-droppable.js} | 0 src/state/move-to-edge.js | 2 +- src/state/reducer.js | 9 +- 9 files changed, 257 insertions(+), 37 deletions(-) rename src/state/{jump-to-next-index.js => jump-to-next-index.old.js} (87%) create mode 100644 src/state/jump-to-next-index/in-foreign-list.js create mode 100644 src/state/jump-to-next-index/in-home-list.js create mode 100644 src/state/jump-to-next-index/index.js create mode 100644 src/state/jump-to-next-index/jump-to-next-index-types.js rename src/state/move-to-best-droppable/{move-to-new-spot.js => move-to-new-droppable.js} (100%) diff --git a/src/state/jump-to-next-index.js b/src/state/jump-to-next-index.old.js similarity index 87% rename from src/state/jump-to-next-index.js rename to src/state/jump-to-next-index.old.js index 48f3c6b0e2..2d3129ac07 100644 --- a/src/state/jump-to-next-index.js +++ b/src/state/jump-to-next-index.old.js @@ -65,15 +65,9 @@ export default ({ const isInHomeList: boolean = draggable.droppableId === droppable.id; if (!isInHomeList) { - console.group('not in home list'); - console.log('not in home list!'); - // if draggable is not in home list const currentIndex: number = location.index; const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; const startIndex: number = impact.foreignDestinationStartIndex; - console.log('proposed index', proposedIndex); - console.log('currentIndex', currentIndex); - console.log('start index', startIndex); // cannot move forward beyond the last item if (proposedIndex > insideDroppable.length) { @@ -159,28 +153,11 @@ export default ({ destinationAxis: droppable.axis, }); - console.log('is moving towards start', isMovingTowardStart); - console.log('is moving forward', isMovingForward); - const moved: DraggableId[] = (() => { - if (isMovingTowardStart) { - // if moving backwards towards the start - if (!isMovingForward) { - // need to trim the existing movement - return [atProposedIndex.id, ...impact.movement.draggables]; - } - return impact.movement.draggables.slice(1, impact.movement.draggables.length); - } - - // if moving away from start and moving backwards if (!isMovingForward) { - // need to add the destination to the impacted + // need to trim the existing movement return [atProposedIndex.id, ...impact.movement.draggables]; } - - // is moving away from the start and moving forward - // need to add the destination to the impacted - // when moving away we need to trim the impacted as it has already moved down to make room return impact.movement.draggables.slice(1, impact.movement.draggables.length); })(); @@ -189,8 +166,9 @@ export default ({ const newImpact: DragImpact = { movement: { draggables: moved, - // The amount of movement will always be the size of the dragging item + // The amount of movement will always be the size of the dragging item amount: patch(axis.line, draggable.page.withMargin[axis.size]), + // when in another list we are never past the start position isBeyondStartPosition: false, }, destination: { @@ -203,8 +181,6 @@ export default ({ // console.log('returning result', { newCenter, newImpact }); - console.groupEnd(); - return { center: newCenter, impact: newImpact, @@ -216,7 +192,7 @@ export default ({ // If not in home list - need to insert draggable into correct position in list // const tempIndex: number = impact.destination.index; - console.log('moving when in home list') + console.log('moving when in home list'); const startIndex: number = getIndex(insideDroppable, draggable); const currentIndex: number = location.index; diff --git a/src/state/jump-to-next-index/in-foreign-list.js b/src/state/jump-to-next-index/in-foreign-list.js new file mode 100644 index 0000000000..345e29c1d0 --- /dev/null +++ b/src/state/jump-to-next-index/in-foreign-list.js @@ -0,0 +1,91 @@ +// @flow +import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; +import { patch } from '../position'; +import moveToEdge from '../move-to-edge'; +import type { Edge } from '../move-to-edge'; +import type { Args, Result } from './jump-to-next-index-types'; +import type { + DraggableLocation, + DraggableDimension, + Position, + Axis, + DragImpact, + DraggableId, +} from '../../types'; + +export default ({ + isMovingForward, + draggableId, + impact, + droppable, + draggables, +}: Args): ?Result => { + console.log('in-foreign-list.js'); + if (!impact.destination) { + console.error('cannot jump to next index when there is not previous destination'); + return null; + } + + const location: DraggableLocation = impact.destination; + const draggable: DraggableDimension = draggables[draggableId]; + const axis: Axis = droppable.axis; + + const insideForeignDroppable: DraggableDimension[] = getDraggablesInsideDroppable( + droppable, + draggables, + ); + + // const startIndex: ?number = impact.foreignDestinationStartIndex; + const currentIndex: number = location.index; + const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; + + // TODO + if (proposedIndex > insideForeignDroppable.length) { + return null; + } + + // cannot move before the first item + if (proposedIndex < 0) { + return null; + } + + const atProposedIndex: DraggableDimension = insideForeignDroppable[proposedIndex]; + const destinationIndex = Math.max(0, proposedIndex - 1); + const destination: DraggableDimension = insideForeignDroppable[destinationIndex]; + console.log('destination', destination.id); + + const sourceEdge: Edge = 'start'; + const destinationEdge: Edge = proposedIndex === 0 ? 'start' : 'end'; + + const newCenter: Position = moveToEdge({ + source: draggable.page.withoutMargin, + sourceEdge, + destination: destination.page.withMargin, + destinationEdge, + destinationAxis: droppable.axis, + }); + + const moved: DraggableId[] = isMovingForward ? + impact.movement.draggables.slice(1, impact.movement.draggables.length) : + [atProposedIndex.id, ...impact.movement.draggables]; + + const newImpact: DragImpact = { + movement: { + draggables: moved, + // The amount of movement will always be the size of the dragging item + amount: patch(axis.line, draggable.page.withMargin[axis.size]), + // when in another list we are never past the start position + isBeyondStartPosition: false, + }, + destination: { + droppableId: droppable.id, + index: proposedIndex, + }, + direction: droppable.axis.direction, + }; + + return { + center: newCenter, + impact: newImpact, + }; +}; diff --git a/src/state/jump-to-next-index/in-home-list.js b/src/state/jump-to-next-index/in-home-list.js new file mode 100644 index 0000000000..fa60d7b1cd --- /dev/null +++ b/src/state/jump-to-next-index/in-home-list.js @@ -0,0 +1,115 @@ +// @flow +import memoizeOne from 'memoize-one'; +import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; +import { patch } from '../position'; +import moveToEdge from '../move-to-edge'; +import type { Edge } from '../move-to-edge'; +import type { Args, Result } from './jump-to-next-index-types'; +import type { + DraggableLocation, + DraggableDimension, + Position, + DraggableId, + Axis, + DragImpact, +} from '../types'; + +const getIndex = memoizeOne( + (draggables: DraggableDimension[], + target: DraggableDimension + ): number => draggables.indexOf(target) +); + +export default ({ + isMovingForward, + draggableId, + impact, + droppable, + draggables, +}: Args): ?Result => { + console.log('in-home-list.js'); + if (!impact.destination) { + console.error('cannot jump to next index when there is not previous destination'); + return null; + } + + const location: DraggableLocation = impact.destination; + const draggable: DraggableDimension = draggables[draggableId]; + const axis: Axis = droppable.axis; + + const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( + droppable, + draggables, + ); + + const startIndex: number = getIndex(insideDroppable, draggable); + const currentIndex: number = location.index; + const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; + + if (startIndex === -1) { + console.error('could not find draggable inside current droppable'); + return null; + } + + // cannot move forward beyond the last item + if (proposedIndex > insideDroppable.length - 1) { + return null; + } + + // cannot move before the first item + if (proposedIndex < 0) { + return null; + } + + const destination: DraggableDimension = insideDroppable[proposedIndex]; + const isMovingTowardStart = (isMovingForward && proposedIndex <= startIndex) || + (!isMovingForward && proposedIndex >= startIndex); + + const edge: Edge = (() => { + // is moving away from the start + if (!isMovingTowardStart) { + return isMovingForward ? 'end' : 'start'; + } + // is moving back towards the start + return isMovingForward ? 'start' : 'end'; + })(); + + const newCenter: Position = moveToEdge({ + source: draggable.page.withoutMargin, + sourceEdge: edge, + destination: destination.page.withoutMargin, + destinationEdge: edge, + destinationAxis: droppable.axis, + }); + + // Calculate DragImpact + + // 1. If moving back towards where we started + // we need to remove the latest addition + // 2. If we are moving away from where we started, + // we need to add the next draggable to the impact + const moved: DraggableId[] = isMovingTowardStart ? + impact.movement.draggables.slice(0, impact.movement.draggables.length - 1) : + [...impact.movement.draggables, destination.id]; + + const newImpact: DragImpact = { + movement: { + draggables: moved, + // 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, + }, + destination: { + droppableId: droppable.id, + index: proposedIndex, + }, + direction: droppable.axis.direction, + }; + + const result: Result = { + center: newCenter, + impact: newImpact, + }; + + return result; +}; diff --git a/src/state/jump-to-next-index/index.js b/src/state/jump-to-next-index/index.js new file mode 100644 index 0000000000..4f6b692e56 --- /dev/null +++ b/src/state/jump-to-next-index/index.js @@ -0,0 +1,18 @@ +// @flow +import inHomeList from './in-home-list'; +import inForeignList from './in-foreign-list'; +import type { Args, Result } from './jump-to-next-index-types'; +import type { DraggableDimension } from '../../types'; + +export default (args: Args): ?Result => { + const { draggableId, draggables, droppable } = args; + + const draggable: DraggableDimension = draggables[draggableId]; + const isInHomeList: boolean = draggable.droppableId === droppable.id; + + if (isInHomeList) { + return inHomeList(args); + } + + return inForeignList(args); +}; diff --git a/src/state/jump-to-next-index/jump-to-next-index-types.js b/src/state/jump-to-next-index/jump-to-next-index-types.js new file mode 100644 index 0000000000..67709e00b5 --- /dev/null +++ b/src/state/jump-to-next-index/jump-to-next-index-types.js @@ -0,0 +1,21 @@ +// @flow +import type { + DraggableId, + Position, + DragImpact, + DroppableDimension, + DraggableDimensionMap, +} from '../../types'; + +export type Args = {| + isMovingForward: boolean, + draggableId: DraggableId, + impact: DragImpact, + droppable: DroppableDimension, + draggables: DraggableDimensionMap, +|} + +export type Result = {| + center: Position, + impact: DragImpact, +|} diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index 33a8e19c1e..0caed04a54 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -2,8 +2,8 @@ import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import getClosestDraggable from './get-closest-draggable'; -import moveToNewSpot from './move-to-new-spot'; -import type { Result } from './move-to-new-spot'; +import moveToNewDroppable from './move-to-new-droppable'; +import type { Result } from './move-to-new-droppable'; import type { DraggableId, DroppableId, @@ -70,7 +70,7 @@ export default ({ draggables, }); - return moveToNewSpot({ + return moveToNewDroppable({ center, draggable, target, diff --git a/src/state/move-to-best-droppable/move-to-new-spot.js b/src/state/move-to-best-droppable/move-to-new-droppable.js similarity index 100% rename from src/state/move-to-best-droppable/move-to-new-spot.js rename to src/state/move-to-best-droppable/move-to-new-droppable.js diff --git a/src/state/move-to-edge.js b/src/state/move-to-edge.js index 8f6f5eac06..40db92eaa6 100644 --- a/src/state/move-to-edge.js +++ b/src/state/move-to-edge.js @@ -6,7 +6,7 @@ import type { DimensionFragment, } from '../types'; -type Edge = 'start' | 'end'; +export type Edge = 'start' | 'end'; type Args = {| source: DimensionFragment, diff --git a/src/state/reducer.js b/src/state/reducer.js index 6fe036ec38..d4c3390946 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -22,10 +22,9 @@ import type { TypeId, } from '../types'; import { add, subtract, negate } from './position'; import getDragImpact from './get-drag-impact'; -import jumpToNextIndex from './jump-to-next-index'; -import type { JumpToNextResult } from './jump-to-next-index'; -import type { Result as MoveToNewSpotResult } from './move-to-best-droppable/move-to-new-spot'; -import getDroppableOver from './get-droppable-over'; +import jumpToNextIndex from './jump-to-next-index/'; +import type { Result as JumpToNextResult } from './jump-to-next-index/jump-to-next-index-types'; +import type { Result as MoveToNewDroppable } from './move-to-best-droppable/move-to-new-droppable'; import moveToBestDroppable from './move-to-best-droppable/'; const noDimensions: DimensionState = { @@ -461,7 +460,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { const droppableId: DroppableId = state.drag.impact.destination.droppableId; const home: DraggableLocation = state.drag.initial.source; - const result: ?MoveToNewSpotResult = moveToBestDroppable({ + const result: ?MoveToNewDroppable = moveToBestDroppable({ isMovingForward: action.type === 'CROSS_AXIS_MOVE_FORWARD', center, draggableId, From 910b229cb0b30ff0d7b5ca36cb08f06750835328 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 10:18:09 +1000 Subject: [PATCH 045/117] removing unneeded property --- .../jump-to-next-index/in-foreign-list.js | 23 ++++++++++++------- .../move-to-new-droppable.js | 1 - src/types.js | 4 ---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/state/jump-to-next-index/in-foreign-list.js b/src/state/jump-to-next-index/in-foreign-list.js index 345e29c1d0..962b0f232c 100644 --- a/src/state/jump-to-next-index/in-foreign-list.js +++ b/src/state/jump-to-next-index/in-foreign-list.js @@ -35,24 +35,27 @@ export default ({ draggables, ); - // const startIndex: ?number = impact.foreignDestinationStartIndex; const currentIndex: number = location.index; + // Where the draggable will end up const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; - // TODO + // draggable is allowed to exceed the foreign droppables count by 1 if (proposedIndex > insideForeignDroppable.length) { return null; } - // cannot move before the first item + // Cannot move before the first item if (proposedIndex < 0) { return null; } const atProposedIndex: DraggableDimension = insideForeignDroppable[proposedIndex]; - const destinationIndex = Math.max(0, proposedIndex - 1); - const destination: DraggableDimension = insideForeignDroppable[destinationIndex]; - console.log('destination', destination.id); + // The draggable that we are going to move relative to + const movingRelativeTo: DraggableDimension = insideForeignDroppable[ + // We want to move relative to the previous draggable + // or to the first if there is no previous + Math.max(0, proposedIndex - 1) + ]; const sourceEdge: Edge = 'start'; const destinationEdge: Edge = proposedIndex === 0 ? 'start' : 'end'; @@ -60,13 +63,17 @@ export default ({ const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, sourceEdge, - destination: destination.page.withMargin, + destination: movingRelativeTo.page.withMargin, destinationEdge, destinationAxis: droppable.axis, }); + // When we are in foreign list we are only displacing items forward + // This list is always sorted by the closest impacted draggable const moved: DraggableId[] = isMovingForward ? + // Stop displacing the closest draggable forward impact.movement.draggables.slice(1, impact.movement.draggables.length) : + // Add the draggable that we are moving into the place of [atProposedIndex.id, ...impact.movement.draggables]; const newImpact: DragImpact = { @@ -74,7 +81,7 @@ export default ({ draggables: moved, // The amount of movement will always be the size of the dragging item amount: patch(axis.line, draggable.page.withMargin[axis.size]), - // when in another list we are never past the start position + // When we are in foreign list we are only displacing items forward isBeyondStartPosition: false, }, destination: { diff --git a/src/state/move-to-best-droppable/move-to-new-droppable.js b/src/state/move-to-best-droppable/move-to-new-droppable.js index 4a882d7f65..cb3df2f901 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable.js @@ -193,7 +193,6 @@ export default ({ droppableId: destination.id, index: proposedIndex, }, - foreignDestinationStartIndex: proposedIndex, }; return { diff --git a/src/types.js b/src/types.js index 3f97dcd6ef..80ce685b93 100644 --- a/src/types.js +++ b/src/types.js @@ -106,10 +106,6 @@ export type DragImpact = {| // the direction of the Droppable you are over direction: ?Direction, destination: ?DraggableLocation, - // The starting index within a droppable - // This might be the original start index - // or it could be the first index received once jumped into a list - foreignDestinationStartIndex?: number, |} export type InitialDragLocation = {| From 9a4ac81a8da20c7fa1c97d7e669ee3fc11480ce1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 11:58:24 +1000 Subject: [PATCH 046/117] working on re entering into home list after transition --- .../move-to-new-droppable.js | 82 ++++++++++++++++--- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/src/state/move-to-best-droppable/move-to-new-droppable.js b/src/state/move-to-best-droppable/move-to-new-droppable.js index cb3df2f901..fca5d6029e 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable.js @@ -2,7 +2,7 @@ import { subtract, patch } from '../position'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import moveToEdge from '../move-to-edge'; -import jumpToNextIndex from '../jump-to-next-index'; +import noImpact from '../no-impact'; import type { Axis, Position, @@ -113,7 +113,8 @@ export default ({ const isReturningToHomeList = destination.id === draggable.droppableId; if (isReturningToHomeList) { - console.log('returning to home list'); + console.group('returning to home list'); + console.log('target index', targetIndex); console.log('proposed index', proposedIndex); // returning to original position if (targetIndex === home.index) { @@ -132,6 +133,7 @@ export default ({ index: 0, }, }; + console.groupEnd(); return { center: newCenter, @@ -139,21 +141,77 @@ export default ({ }; } - // TODO: need to give an appropriate impact! - // TODO: broken if moving back to list when current list is impacted console.info('returning to original list - but not in original position'); + console.log('is going before target', isGoingBeforeTarget); - const result = jumpToNextIndex({ - isMovingForward: isGoingBeforeTarget, - draggableId: draggable.id, - impact, - draggables, - droppable: destination, + // need to put into the correct position and have the correct impact + + const isMovingBeyondHome = targetIndex > home.index; + console.log('is moving beyond home', isMovingBeyondHome); + + const isMovingRelativeToSelf = target.id === draggable.id; + console.log('target id', target.id); + + console.log('is moving relative to self', isMovingRelativeToSelf); + + const sourceEdge = (() => { + if (isMovingBeyondHome) { + return 'start'; + } + return 'start'; + })(); + + const destinationEdge = (() => { + if (isMovingBeyondHome) { + return 'end'; + } + return 'start'; + })(); + + const newCenter: Position = moveToEdge({ + source: draggable.page.withoutMargin, + sourceEdge, + destination: target.page.withMargin, + destinationEdge, + destinationAxis, }); - console.log('result moving back home', result); + const needsToMove: DraggableId[] = (() => { + if (isMovingBeyondHome) { + console.group('movingBeyondHome'); + console.log('original', insideDestination); + const result = [...insideDestination]; + result.splice(home.index, 1); + console.log('stripped', result); + return result.slice(0, proposedIndex); + console.groupEnd(); + } + return insideDestination.slice(proposedIndex, home.index); + })().map(d => d.id); + + console.log('moved', needsToMove); - return result; + const newImpact: DragImpact = { + movement: { + draggables: needsToMove, + amount, + // TODO: not sure what this should be + isBeyondStartPosition: isMovingBeyondHome, + }, + direction: destinationAxis.direction, + destination: { + droppableId: destination.id, + index: proposedIndex, + }, + }; + + console.log('impact', newImpact); + console.groupEnd(); + + return { + center: newCenter, + impact: newImpact, + }; } // 1. If isGoingBefore: need to move draggable start edge to start edge of target From 5fd90b2c246fdfe6056bccdcb7dddbb965fc6870 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 14:43:01 +1000 Subject: [PATCH 047/117] wip --- src/state/jump-to-next-index/in-home-list.js | 3 +++ .../move-to-new-droppable.js | 15 ++++++++++----- stories/src/data.js | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/state/jump-to-next-index/in-home-list.js b/src/state/jump-to-next-index/in-home-list.js index fa60d7b1cd..52b79d46cf 100644 --- a/src/state/jump-to-next-index/in-home-list.js +++ b/src/state/jump-to-next-index/in-home-list.js @@ -46,6 +46,9 @@ export default ({ const currentIndex: number = location.index; const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; + console.log('proposed index', proposedIndex); + console.log('current index', currentIndex); + if (startIndex === -1) { console.error('could not find draggable inside current droppable'); return null; diff --git a/src/state/move-to-best-droppable/move-to-new-droppable.js b/src/state/move-to-best-droppable/move-to-new-droppable.js index fca5d6029e..bed3bbefe3 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable.js @@ -113,6 +113,8 @@ export default ({ const isReturningToHomeList = destination.id === draggable.droppableId; if (isReturningToHomeList) { + const proposedIndex: number = targetIndex; + console.group('returning to home list'); console.log('target index', targetIndex); console.log('proposed index', proposedIndex); @@ -130,7 +132,7 @@ export default ({ direction: destinationAxis.direction, destination: { droppableId: destination.id, - index: 0, + index: home.index, }, }; console.groupEnd(); @@ -156,18 +158,21 @@ export default ({ const sourceEdge = (() => { if (isMovingBeyondHome) { - return 'start'; + return isGoingBeforeTarget ? 'end' : 'end'; } return 'start'; })(); const destinationEdge = (() => { if (isMovingBeyondHome) { - return 'end'; + return isGoingBeforeTarget ? 'end' : 'end'; } return 'start'; })(); + console.log('source edge', sourceEdge); + console.log('destination edge', destinationEdge); + const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, sourceEdge, @@ -181,9 +186,9 @@ export default ({ console.group('movingBeyondHome'); console.log('original', insideDestination); const result = [...insideDestination]; - result.splice(home.index, 1); + // result.splice(home.index, 1); console.log('stripped', result); - return result.slice(0, proposedIndex); + return result.slice(home.index + 1, proposedIndex + 1); console.groupEnd(); } return insideDestination.slice(proposedIndex, home.index); diff --git a/stories/src/data.js b/stories/src/data.js index 22a8775ed9..366c2c4253 100644 --- a/stories/src/data.js +++ b/stories/src/data.js @@ -79,6 +79,21 @@ export const quotes: Quote[] = [ content: 'Don\'t you always call sweatpants \'give up on life pants,\' Jake?', author: finn, }, + { + id: '10', + content: 'I should not have drunk that much tea!', + author: princess, + }, + { + id: '11', + content: 'Please! I need the real you!', + author: princess, + }, + { + id: '12', + content: 'Haven\'t slept for a solid 83 hours, but, yeah, I\'m good.', + author: princess, + }, ]; export const getQuotes = (count: number): Quote[] => From c18b8331d504c61b945817c2e472ea22721c3855 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 15:49:03 +1000 Subject: [PATCH 048/117] refactoring move-to-new-droppable --- src/state/jump-to-next-index/in-home-list.js | 5 +- src/state/move-to-best-droppable/index.js | 4 +- ...ppable.js => move-to-new-droppable.old.js} | 1 - .../move-to-new-droppable/index.js | 75 +++++++++++ .../move-to-new-droppable-types.js | 9 ++ .../move-to-new-droppable/to-foreign-list.js | 109 ++++++++++++++++ .../move-to-new-droppable/to-home-list.js | 119 ++++++++++++++++++ 7 files changed, 315 insertions(+), 7 deletions(-) rename src/state/move-to-best-droppable/{move-to-new-droppable.js => move-to-new-droppable.old.js} (99%) create mode 100644 src/state/move-to-best-droppable/move-to-new-droppable/index.js create mode 100644 src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js create mode 100644 src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js create mode 100644 src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js diff --git a/src/state/jump-to-next-index/in-home-list.js b/src/state/jump-to-next-index/in-home-list.js index 52b79d46cf..c0b0ee2fcb 100644 --- a/src/state/jump-to-next-index/in-home-list.js +++ b/src/state/jump-to-next-index/in-home-list.js @@ -12,7 +12,7 @@ import type { DraggableId, Axis, DragImpact, -} from '../types'; +} from '../../types'; const getIndex = memoizeOne( (draggables: DraggableDimension[], @@ -46,9 +46,6 @@ export default ({ const currentIndex: number = location.index; const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; - console.log('proposed index', proposedIndex); - console.log('current index', currentIndex); - if (startIndex === -1) { console.error('could not find draggable inside current droppable'); return null; diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index 0caed04a54..cd8893603e 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -1,8 +1,7 @@ // @flow import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; -import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import getClosestDraggable from './get-closest-draggable'; -import moveToNewDroppable from './move-to-new-droppable'; +import moveToNewDroppable from './move-to-new-droppable/'; import type { Result } from './move-to-new-droppable'; import type { DraggableId, @@ -13,6 +12,7 @@ import type { DraggableDimensionMap, DroppableDimensionMap, DraggableLocation, + DragImpact, } from '../../types'; type Args = {| diff --git a/src/state/move-to-best-droppable/move-to-new-droppable.js b/src/state/move-to-best-droppable/move-to-new-droppable.old.js similarity index 99% rename from src/state/move-to-best-droppable/move-to-new-droppable.js rename to src/state/move-to-best-droppable/move-to-new-droppable.old.js index bed3bbefe3..88e08e647d 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable.old.js @@ -48,7 +48,6 @@ export default ({ draggable, target, home, - impact, draggables, }: Args): ?Result => { const destinationAxis: Axis = destination.axis; diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/index.js b/src/state/move-to-best-droppable/move-to-new-droppable/index.js new file mode 100644 index 0000000000..bc78e12c92 --- /dev/null +++ b/src/state/move-to-best-droppable/move-to-new-droppable/index.js @@ -0,0 +1,75 @@ +// @flow +import toHomeList from './to-home-list'; +import toForeignList from './to-foreign-list'; +import { patch } from '../../position'; +import getDraggablesInsideDroppable from '../../get-draggables-inside-droppable'; +import type { Result } from './move-to-new-droppable-types'; +import type { + Position, + DragImpact, + DraggableDimension, + DroppableDimension, + DraggableLocation, + DraggableDimensionMap, +} from '../../../types'; + +type Args = {| + // the current center position of the draggable + center: Position, + // the draggable that is dragging and needs to move + draggable: DraggableDimension, + // what the draggable is moving towards + // can be null if the destination is empty + target: ?DraggableDimension, + // the droppable the draggable is moving to + destination: DroppableDimension, + // the source location of the draggable + home: DraggableLocation, + // the current drag impact + impact: DragImpact, + // all the draggables in the system + draggables: DraggableDimensionMap, +|} + +export default ({ + center, + destination, + draggable, + target, + home, + draggables, +}: Args): ?Result => { + const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable( + destination, draggables + ); + const amount: Position = patch( + destination.axis.line, + draggable.page.withMargin[destination.axis.size] + ); + + const isGoingBeforeTarget: boolean = Boolean(target && + center[destination.axis.line] < target.page.withMargin.center[destination.axis.line]); + + // moving back to the home list + if (destination.id === draggable.droppableId) { + return toHomeList({ + amount, + isGoingBeforeTarget, + originalIndex: home.index, + target, + insideDroppable: insideDestination, + draggable, + droppable: destination, + }); + } + + // moving to a foreign list + return toForeignList({ + amount, + isGoingBeforeTarget, + target, + insideDroppable: insideDestination, + draggable, + droppable: destination, + }); +}; diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js b/src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js new file mode 100644 index 0000000000..ae6d9079ed --- /dev/null +++ b/src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js @@ -0,0 +1,9 @@ +// @flow +import type { Position, DragImpact } from '../../../types'; + +export type Result = {| + // how far the draggable needs to move to be in its new home + center: Position, + // The impact of the movement + impact: DragImpact, +|} diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js new file mode 100644 index 0000000000..5ee594c814 --- /dev/null +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js @@ -0,0 +1,109 @@ +// @flow +import moveToEdge from '../../move-to-edge'; +import type { Result } from './move-to-new-droppable-types'; +import type { + Axis, + Position, + DragImpact, + DraggableId, + DraggableDimension, + DroppableDimension, +} from '../../../types'; + +type Args = {| + amount: Position, + isGoingBeforeTarget: boolean, + target: ?DraggableDimension, + insideDroppable: DraggableDimension[], + draggable: DraggableDimension, + droppable: DroppableDimension, +|} + +export default ({ + amount, + isGoingBeforeTarget, + target, + insideDroppable, + draggable, + droppable, +}: Args): ?Result => { + console.log('to-foreign-list.js'); + const axis: Axis = droppable.axis; + + // Moving to an empty list + + if (!target) { + // Move to start edge of the destination + // based on the axis of the destination + + const newCenter: Position = moveToEdge({ + source: draggable.page.withMargin, + sourceEdge: 'start', + destination: droppable.page.withMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); + + const newImpact: DragImpact = { + movement: { + draggables: [], + amount, + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: droppable.id, + index: 0, + }, + }; + + return { + center: newCenter, + impact: newImpact, + }; + } + + // Moving to a populated list + + const targetIndex: number = insideDroppable.indexOf(target); + const proposedIndex: number = isGoingBeforeTarget ? targetIndex : targetIndex + 1; + + if (targetIndex === -1) { + console.error('could not find target inside destination'); + return null; + } + if (droppable.id === draggable.droppableId) { + console.error('to-foreign-list handles movement to foreign lists and not home lists'); + return null; + } + + const newCenter: Position = moveToEdge({ + source: draggable.page.withoutMargin, + sourceEdge: 'start', + destination: target.page.withMargin, + destinationEdge: isGoingBeforeTarget ? 'start' : 'end', + destinationAxis: axis, + }); + + const needsToMove: DraggableId[] = insideDroppable + .slice(proposedIndex, insideDroppable.length) + .map((dimension: DraggableDimension): DraggableId => dimension.id); + + const newImpact: DragImpact = { + movement: { + draggables: needsToMove, + amount, + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: droppable.id, + index: proposedIndex, + }, + }; + + return { + center: newCenter, + impact: newImpact, + }; +}; diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js new file mode 100644 index 0000000000..2c125c8450 --- /dev/null +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js @@ -0,0 +1,119 @@ +// @flow +import moveToEdge from '../../move-to-edge'; +import type { Result } from './move-to-new-droppable-types'; +import type { + Axis, + Position, + DragImpact, + DraggableId, + DraggableDimension, + DroppableDimension, +} from '../../../types'; + +type Args = {| + amount: Position, + isGoingBeforeTarget: boolean, + originalIndex: number, + target: ?DraggableDimension, + insideDroppable: DraggableDimension[], + draggable: DraggableDimension, + droppable: DroppableDimension, +|} + +export default ({ + isGoingBeforeTarget, + originalIndex, + target, + amount, + insideDroppable, + draggable, + droppable, +}: Args): ?Result => { + console.log('to-home-list.js'); + if (!target) { + console.error('there will always be a target in the original list'); + return null; + } + + const axis: Axis = droppable.axis; + const targetIndex: number = insideDroppable.indexOf(target); + + if (targetIndex === -1) { + console.error('unable to find target in destination droppable'); + return null; + } + + // Moving back to original index + // Super simple - just move it back to the original center with no impact + if (targetIndex === originalIndex) { + const newCenter: Position = draggable.page.withoutMargin.center; + const newImpact: DragImpact = { + movement: { + draggables: [], + amount, + isBeyondStartPosition: false, + }, + direction: droppable.axis.direction, + destination: { + droppableId: droppable.id, + index: originalIndex, + }, + }; + + return { + center: newCenter, + impact: newImpact, + }; + } + + const isMovingBeyondHome = targetIndex > originalIndex; + + const sourceEdge = (() => { + if (isMovingBeyondHome) { + return isGoingBeforeTarget ? 'end' : 'end'; + } + return 'start'; + })(); + + const destinationEdge = (() => { + if (isMovingBeyondHome) { + return isGoingBeforeTarget ? 'end' : 'end'; + } + return 'start'; + })(); + + const newCenter: Position = moveToEdge({ + source: draggable.page.withoutMargin, + sourceEdge, + destination: target.page.withMargin, + destinationEdge, + destinationAxis: axis, + }); + + const needsToMove: DraggableId[] = (() => { + if (isMovingBeyondHome) { + const result = [...insideDroppable]; + return result.slice(originalIndex + 1, targetIndex + 1); + } + return insideDroppable.slice(targetIndex, originalIndex); + })().map(d => d.id); + + const newImpact: DragImpact = { + movement: { + draggables: needsToMove, + amount, + // TODO: not sure what this should be + isBeyondStartPosition: isMovingBeyondHome, + }, + direction: axis.direction, + destination: { + droppableId: droppable.id, + index: targetIndex, + }, + }; + + return { + center: newCenter, + impact: newImpact, + }; +}; From 0fdde590e0afe471e04469f23e61f7603746ffae Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 16:04:28 +1000 Subject: [PATCH 049/117] returning to home list now working much better --- .../move-to-new-droppable/to-home-list.js | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js index 2c125c8450..772c367ff7 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js @@ -1,5 +1,6 @@ // @flow import moveToEdge from '../../move-to-edge'; +import type { Edge } from '../../move-to-edge'; import type { Result } from './move-to-new-droppable-types'; import type { Axis, @@ -68,35 +69,22 @@ export default ({ const isMovingBeyondHome = targetIndex > originalIndex; - const sourceEdge = (() => { - if (isMovingBeyondHome) { - return isGoingBeforeTarget ? 'end' : 'end'; - } - return 'start'; - })(); - - const destinationEdge = (() => { - if (isMovingBeyondHome) { - return isGoingBeforeTarget ? 'end' : 'end'; - } - return 'start'; - })(); + const edge: Edge = isMovingBeyondHome ? 'end' : 'start'; const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, - sourceEdge, - destination: target.page.withMargin, - destinationEdge, + sourceEdge: edge, + destination: isMovingBeyondHome ? target.page.withoutMargin : target.page.withMargin, + destinationEdge: edge, destinationAxis: axis, }); const needsToMove: DraggableId[] = (() => { if (isMovingBeyondHome) { - const result = [...insideDroppable]; - return result.slice(originalIndex + 1, targetIndex + 1); + return insideDroppable.slice(originalIndex + 1, targetIndex + 1); } return insideDroppable.slice(targetIndex, originalIndex); - })().map(d => d.id); + })().map((d: DraggableDimension): DraggableId => d.id); const newImpact: DragImpact = { movement: { From a146a0b7916359959ee3c37b6f447d9e7c8c8dd0 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 16:33:20 +1000 Subject: [PATCH 050/117] making the order of the moved array consistant between files --- src/state/jump-to-next-index/in-home-list.js | 10 ++++------ .../move-to-new-droppable/index.js | 1 - .../move-to-new-droppable/to-foreign-list.js | 2 ++ .../move-to-new-droppable/to-home-list.js | 8 ++++++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/state/jump-to-next-index/in-home-list.js b/src/state/jump-to-next-index/in-home-list.js index c0b0ee2fcb..0c6892f84e 100644 --- a/src/state/jump-to-next-index/in-home-list.js +++ b/src/state/jump-to-next-index/in-home-list.js @@ -84,13 +84,11 @@ export default ({ // Calculate DragImpact - // 1. If moving back towards where we started - // we need to remove the latest addition - // 2. If we are moving away from where we started, - // we need to add the next draggable to the impact const moved: DraggableId[] = isMovingTowardStart ? - impact.movement.draggables.slice(0, impact.movement.draggables.length - 1) : - [...impact.movement.draggables, destination.id]; + impact.movement.draggables.slice(1, impact.movement.draggables.length) : + [destination.id, ...impact.movement.draggables]; + + console.log('in home list moved', moved); const newImpact: DragImpact = { movement: { diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/index.js b/src/state/move-to-best-droppable/move-to-new-droppable/index.js index bc78e12c92..3b26f1f955 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/index.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/index.js @@ -54,7 +54,6 @@ export default ({ if (destination.id === draggable.droppableId) { return toHomeList({ amount, - isGoingBeforeTarget, originalIndex: home.index, target, insideDroppable: insideDestination, diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js index 5ee594c814..93d346f712 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js @@ -89,6 +89,8 @@ export default ({ .slice(proposedIndex, insideDroppable.length) .map((dimension: DraggableDimension): DraggableId => dimension.id); + console.log('moved', needsToMove); + const newImpact: DragImpact = { movement: { draggables: needsToMove, diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js index 772c367ff7..d851f7bacc 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js @@ -22,7 +22,6 @@ type Args = {| |} export default ({ - isGoingBeforeTarget, originalIndex, target, amount, @@ -84,7 +83,12 @@ export default ({ return insideDroppable.slice(originalIndex + 1, targetIndex + 1); } return insideDroppable.slice(targetIndex, originalIndex); - })().map((d: DraggableDimension): DraggableId => d.id); + })() + .map((d: DraggableDimension): DraggableId => d.id) + // need to ensure that the list is sorted with the closest item being first + .reverse(); + + console.log('to home list moved', needsToMove); const newImpact: DragImpact = { movement: { From 3dead3dea63f2da7f3a3081cdd1a323663db8838 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 21:20:25 +1000 Subject: [PATCH 051/117] updating stories --- stories/5-multiple-vertical-lists-story.js | 2 +- stories/6-multiple-horizontal-lists-story.js | 23 +++ stories/src/horizontal/author-app.jsx | 22 ++- stories/src/horizontal/author-list.jsx | 1 + stories/src/multiple-horizontal/quote-app.jsx | 151 ++++++++++++++++++ .../src/multiple-horizontal/quote-list.jsx | 108 +++++++++++++ stories/src/multiple-vertical/quote-app.jsx | 6 +- stories/src/multiple-vertical/quote-list.jsx | 17 +- .../author-item.jsx | 31 ++-- 9 files changed, 327 insertions(+), 34 deletions(-) create mode 100644 stories/6-multiple-horizontal-lists-story.js create mode 100644 stories/src/multiple-horizontal/quote-app.jsx create mode 100644 stories/src/multiple-horizontal/quote-list.jsx rename stories/src/{horizontal => primatives}/author-item.jsx (62%) diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js index dacce84b46..d37b790a36 100644 --- a/stories/5-multiple-vertical-lists-story.js +++ b/stories/5-multiple-vertical-lists-story.js @@ -19,6 +19,6 @@ const initialQuotes = { }; storiesOf('multiple vertical lists', module) - .add('simple example', () => ( + .add('stress test', () => ( )); diff --git a/stories/6-multiple-horizontal-lists-story.js b/stories/6-multiple-horizontal-lists-story.js new file mode 100644 index 0000000000..b18fb57b1c --- /dev/null +++ b/stories/6-multiple-horizontal-lists-story.js @@ -0,0 +1,23 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import QuoteApp from './src/multiple-horizontal/quote-app'; +import { getQuotes } from './src/data'; + +const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( + quote => ({ + ...quote, + id: `${namespace}::${quote.id}`, + }) +); + +const initialQuotes = { + alpha: namespaceQuoteIds(getQuotes(10), 'alpha'), + beta: namespaceQuoteIds(getQuotes(8), 'beta'), + gamma: namespaceQuoteIds(getQuotes(12), 'gamma'), +}; + +storiesOf('multiple horizontal lists', module) + .add('stress test', () => ( + + )); diff --git a/stories/src/horizontal/author-app.jsx b/stories/src/horizontal/author-app.jsx index a0bfaf88a5..1073ad3784 100644 --- a/stories/src/horizontal/author-app.jsx +++ b/stories/src/horizontal/author-app.jsx @@ -5,7 +5,8 @@ import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import type { DropResult, DragStart } from '../../../src'; import AuthorList from './author-list'; -import AuthorItem from './author-item'; +import AuthorItem from '../primatives/author-item'; +import { Draggable } from '../../../src/'; import reorder from '../reorder'; import { colors, grid } from '../constants'; import type { Author } from '../types'; @@ -85,10 +86,23 @@ export default class AuthorApp extends Component { {this.state.authors.map((author: Author) => ( - + draggableId={author.id} + type="list" + > + {(provided, snapshot) => ( +
+ + {provided.placeholder} +
+ )} + + ))}
diff --git a/stories/src/horizontal/author-list.jsx b/stories/src/horizontal/author-list.jsx index d965d7b1f1..f2ba588e2e 100644 --- a/stories/src/horizontal/author-list.jsx +++ b/stories/src/horizontal/author-list.jsx @@ -34,6 +34,7 @@ export default class AuthorList extends Component { innerRef={provided.innerRef} > {this.props.children} + {provided.placeholder} )} diff --git a/stories/src/multiple-horizontal/quote-app.jsx b/stories/src/multiple-horizontal/quote-app.jsx new file mode 100644 index 0000000000..471534ade7 --- /dev/null +++ b/stories/src/multiple-horizontal/quote-app.jsx @@ -0,0 +1,151 @@ +// @flow +import React, { Component } from 'react'; +import styled, { injectGlobal } from 'styled-components'; +import { action } from '@storybook/addon-actions'; +import { DragDropContext } from '../../../src/'; +import QuoteList from './quote-list'; +import { colors, grid } from '../constants'; +import type { Quote } from '../types'; +import type { DropResult, DragStart, DraggableLocation } from '../../../src/types'; + +const publishOnDragStart = action('onDragStart'); +const publishOnDragEnd = action('onDragEnd'); + +const Root = styled.div` + background-color: ${colors.blue.deep}; + box-sizing: border-box; + padding: ${grid * 2}px; + min-height: 100vh; + + /* flexbox */ + display: flex; + flex-direction: column; +`; + +const Row = styled.div` + margin: ${grid * 2}px ${grid * 2}px; +`; + +const PushDown = styled.div` + height: 200px; +`; + +const isDraggingClassName = 'is-dragging'; + +type GroupedQuotes = { + alpha: Quote[], + beta: Quote[], + gamma: Quote[], +} + +type Props = {| + initial: GroupedQuotes, +|} + +type State = {| + quotes: GroupedQuotes, +|} + +const resolveDrop = (quotes: GroupedQuotes, + source: DraggableLocation, + destination: DraggableLocation +): GroupedQuotes => { + const newQuotes: GroupedQuotes = { ...quotes }; + + const movedQuote = quotes[source.droppableId][source.index]; + + Object.entries(newQuotes).forEach(([listId, listQuotes]: [string, Quote[]]) => { + let newListQuotes = [...listQuotes]; + + if (listId === source.droppableId) { + newListQuotes = [ + ...newListQuotes.slice(0, source.index), + ...newListQuotes.slice(source.index + 1), + ]; + } + + if (listId === destination.droppableId) { + newListQuotes = [ + ...newListQuotes.slice(0, destination.index), + movedQuote, + ...newListQuotes.slice(destination.index), + ]; + } + + newQuotes[listId] = newListQuotes; + }); + + return newQuotes; +}; + +export default class QuoteApp extends Component { + /* eslint-disable react/sort-comp */ + props: Props + state: State + + state: State = { + quotes: this.props.initial, + }; + /* eslint-enable react/sort-comp */ + + 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) { + return; + } + + const quotes = resolveDrop(this.state.quotes, result.source, result.destination); + + this.setState({ quotes }); + } + + componentDidMount() { + // eslint-disable-next-line no-unused-expressions + injectGlobal` + body.${isDraggingClassName} { + cursor: grabbing; + user-select: none; + } + `; + } + + render() { + const { quotes } = this.state; + + return ( + + + + + + + + ); + } +} diff --git a/stories/src/multiple-horizontal/quote-list.jsx b/stories/src/multiple-horizontal/quote-list.jsx new file mode 100644 index 0000000000..77b225a61c --- /dev/null +++ b/stories/src/multiple-horizontal/quote-list.jsx @@ -0,0 +1,108 @@ +// @flow +import React, { Component } from 'react'; +import styled from 'styled-components'; +import { Droppable, Draggable } from '../../../src'; +import Author from '../primatives/author-item'; +import { grid, colors } from '../constants'; +import type { Quote } from '../types'; +import type { + DroppableProvided, + DroppableStateSnapshot, + DraggableProvided, + DraggableStateSnapshot, +} from '../../../src/'; + +const Wrapper = styled.div` + background-color: ${({ isDraggingOver }) => (isDraggingOver ? colors.blue.lighter : colors.blue.light)}; + display: flex; + flex-direction: column; + padding: ${grid}px; + user-select: none; + transition: background-color 0.1s ease; + margin: ${grid}px 0; + + overflow: auto; +`; + +const DropZone = styled.div` + /* stop the list collapsing when empty */ + min-width: 600px; + display: flex; +`; + +const ScrollContainer = styled.div` + overflow: auto; +`; + +const Container = styled.div` + /* flex child */ + flex-grow: 1; + + /* flex parent */ + display: flex; + flex-direction: column; +`; + +const Title = styled.h4` + margin-bottom: ${grid}px; +`; + +export default class QuoteList extends Component { + props: {| + listId: string, + quotes: Quote[], + listType?: string, + style?: Object, + internalScroll ?: boolean, + |} + + renderBoard = (dropProvided: DroppableProvided) => { + const { listId, listType, quotes } = this.props; + + return ( + + {listId} + + {quotes.map((quote: Quote) => ( + + {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( +
+ + {dragProvided.placeholder} +
+ )} +
+ ))} + {dropProvided.placeholder} +
+
+ ); + } + + render() { + const { listId, listType, internalScroll, style } = this.props; + + return ( + + {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( + + {internalScroll ? ( + + {this.renderBoard(dropProvided)} + + ) : ( + this.renderBoard(dropProvided) + )} + + )} + + ); + } +} diff --git a/stories/src/multiple-vertical/quote-app.jsx b/stories/src/multiple-vertical/quote-app.jsx index d6ece527da..a856ea12e2 100644 --- a/stories/src/multiple-vertical/quote-app.jsx +++ b/stories/src/multiple-vertical/quote-app.jsx @@ -5,7 +5,6 @@ import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import QuoteList from './quote-list'; import { colors, grid } from '../constants'; -import reorder from '../reorder'; import type { Quote } from '../types'; import type { DropResult, DragStart, DraggableLocation } from '../../../src/types'; @@ -49,7 +48,10 @@ type State = {| quotes: GroupedQuotes, |} -const resolveDrop = (quotes: GroupedQuotes, source: DraggableLocation, destination: DraggableLocation): GroupedQuotes => { +const resolveDrop = (quotes: GroupedQuotes, + source: DraggableLocation, + destination: DraggableLocation +): GroupedQuotes => { const newQuotes: GroupedQuotes = { ...quotes }; const movedQuote = quotes[source.droppableId][source.index]; diff --git a/stories/src/multiple-vertical/quote-list.jsx b/stories/src/multiple-vertical/quote-list.jsx index 7a2f4f04b0..a6fcbce604 100644 --- a/stories/src/multiple-vertical/quote-list.jsx +++ b/stories/src/multiple-vertical/quote-list.jsx @@ -6,13 +6,11 @@ import QuoteItem from '../primatives/quote-item'; import { grid, colors } from '../constants'; import type { Quote } from '../types'; import type { - Provided as DroppableProvided, - StateSnapshot as DroppableStateSnapshot, -} from '../../../src/view/droppable/droppable-types'; -import type { - Provided as DraggableProvided, - StateSnapshot as DraggableStateSnapshot, -} from '../../../src/view/draggable/draggable-types'; + DroppableProvided, + DroppableStateSnapshot, + DraggableProvided, + DraggableStateSnapshot, +} from '../../../src/'; const Wrapper = styled.div` width: 250px; @@ -31,8 +29,7 @@ const DropZone = styled.div` `; const ScrollContainer = styled.div` - overflow-x: hidden; - overflow-y: auto; + overflow: auto; max-height: 400px; `; @@ -55,7 +52,7 @@ export default class QuoteList extends Component { quotes: Quote[], listType?: string, style?: Object, - internalScroll?: boolean, + internalScroll ?: boolean, |} renderBoard = (dropProvided: DroppableProvided) => { diff --git a/stories/src/horizontal/author-item.jsx b/stories/src/primatives/author-item.jsx similarity index 62% rename from stories/src/horizontal/author-item.jsx rename to stories/src/primatives/author-item.jsx index 10dae79ba8..36391dc938 100644 --- a/stories/src/horizontal/author-item.jsx +++ b/stories/src/primatives/author-item.jsx @@ -2,7 +2,6 @@ import React, { Component } from 'react'; import styled from 'styled-components'; import { colors, grid } from '../constants'; -import { Draggable } from '../../../src/'; import type { DraggableProvided, DraggableStateSnapshot } from '../../../src/'; import type { Author } from '../types'; @@ -28,27 +27,25 @@ const Avatar = styled.img` export default class AuthorItem extends Component { props: {| - author: Author + author: Author, + provided: DraggableProvided, + snapshot: DraggableStateSnapshot, |} render() { const author: Author = this.props.author; + const provided: DraggableProvided = this.props.provided; + const snapshot: DraggableStateSnapshot = this.props.snapshot; + return ( - - {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( -
- provided.innerRef(ref)} - style={provided.draggableStyle} - {...provided.dragHandleProps} - src={author.avatarUrl} - alt={author.name} - isDragging={snapshot.isDragging} - /> - {provided.placeholder} -
- )} -
+ provided.innerRef(ref)} + style={provided.draggableStyle} + {...provided.dragHandleProps} + src={author.avatarUrl} + alt={author.name} + isDragging={snapshot.isDragging} + /> ); } } From a8837166b8e7b208b1fa978ba83605d91b976cec Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 6 Sep 2017 21:26:12 +1000 Subject: [PATCH 052/117] fixing story --- .../move-to-best-droppable/get-closest-draggable.js | 1 + stories/src/horizontal/author-app.jsx | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index 5787525453..f243914e60 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -38,6 +38,7 @@ export default ({ siblings .filter((draggable: DraggableDimension) => isVisible(draggable, destination)) .sort((a: DraggableDimension, b: DraggableDimension): number => { + // TODO: not considering scroll offset const distanceToA = distance(center, add(a.page.withMargin.center, scrollOffset)); const distanceToB = distance(center, add(b.page.withMargin.center, scrollOffset)); diff --git a/stories/src/horizontal/author-app.jsx b/stories/src/horizontal/author-app.jsx index 1073ad3784..c2ad48b5a6 100644 --- a/stories/src/horizontal/author-app.jsx +++ b/stories/src/horizontal/author-app.jsx @@ -2,11 +2,15 @@ import React, { Component } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { action } from '@storybook/addon-actions'; -import { DragDropContext } from '../../../src/'; -import type { DropResult, DragStart } from '../../../src'; +import { Draggable, DragDropContext } from '../../../src/'; +import type { + DropResult, + DragStart, + DraggableProvided, + DraggableStateSnapshot, +} from '../../../src'; import AuthorList from './author-list'; import AuthorItem from '../primatives/author-item'; -import { Draggable } from '../../../src/'; import reorder from '../reorder'; import { colors, grid } from '../constants'; import type { Author } from '../types'; @@ -89,9 +93,8 @@ export default class AuthorApp extends Component { - {(provided, snapshot) => ( + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
Date: Wed, 6 Sep 2017 22:00:25 +1000 Subject: [PATCH 053/117] trying to get scroll working --- src/state/move-to-best-droppable/get-closest-draggable.js | 5 +++-- stories/6-multiple-horizontal-lists-story.js | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index f243914e60..4b3b6bb341 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -39,8 +39,9 @@ export default ({ .filter((draggable: DraggableDimension) => isVisible(draggable, destination)) .sort((a: DraggableDimension, b: DraggableDimension): number => { // TODO: not considering scroll offset - const distanceToA = distance(center, add(a.page.withMargin.center, scrollOffset)); - const distanceToB = distance(center, add(b.page.withMargin.center, scrollOffset)); + console.log('scroll offset', scrollOffset); + const distanceToA = distance(center, a.page.withMargin.center); + const distanceToB = distance(center, b.page.withMargin.center); // if a is closer - return a if (distanceToA < distanceToB) { diff --git a/stories/6-multiple-horizontal-lists-story.js b/stories/6-multiple-horizontal-lists-story.js index b18fb57b1c..26dee7ce93 100644 --- a/stories/6-multiple-horizontal-lists-story.js +++ b/stories/6-multiple-horizontal-lists-story.js @@ -12,9 +12,9 @@ const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( ); const initialQuotes = { - alpha: namespaceQuoteIds(getQuotes(10), 'alpha'), - beta: namespaceQuoteIds(getQuotes(8), 'beta'), - gamma: namespaceQuoteIds(getQuotes(12), 'gamma'), + alpha: namespaceQuoteIds(getQuotes(20), 'alpha'), + beta: namespaceQuoteIds(getQuotes(18), 'beta'), + gamma: namespaceQuoteIds(getQuotes(22), 'gamma'), }; storiesOf('multiple horizontal lists', module) From c7e19c54e11a75d1dfcb9eea448f180bee404b7d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 08:14:56 +1000 Subject: [PATCH 054/117] more example data --- stories/5-multiple-vertical-lists-story.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js index d37b790a36..51683b2ec8 100644 --- a/stories/5-multiple-vertical-lists-story.js +++ b/stories/5-multiple-vertical-lists-story.js @@ -12,7 +12,7 @@ const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( ); const initialQuotes = { - alpha: namespaceQuoteIds(getQuotes(10), 'alpha'), + alpha: namespaceQuoteIds(getQuotes(20), 'alpha'), beta: namespaceQuoteIds(getQuotes(3), 'beta'), gamma: namespaceQuoteIds(getQuotes(10), 'gamma'), delta: namespaceQuoteIds(getQuotes(0), 'delta'), From 177d71ef55d375cad9867b405e1f275fe6af8b72 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 09:29:53 +1000 Subject: [PATCH 055/117] now handling window scroll correctly for keyboard movements --- src/state/jump-to-next-index/in-foreign-list.js | 2 +- src/state/jump-to-next-index/in-home-list.js | 2 +- .../jump-to-next-index-types.js | 4 +++- .../get-best-cross-axis-droppable.js | 10 +++++----- .../get-closest-draggable.js | 8 ++++---- src/state/move-to-best-droppable/index.js | 12 ++++++------ .../move-to-new-droppable/index.js | 6 +++--- .../move-to-new-droppable-types.js | 2 +- .../move-to-new-droppable/to-foreign-list.js | 4 ++-- .../move-to-new-droppable/to-home-list.js | 4 ++-- src/state/reducer.js | 16 +++++++++++----- src/types.js | 6 +++--- 12 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/state/jump-to-next-index/in-foreign-list.js b/src/state/jump-to-next-index/in-foreign-list.js index 962b0f232c..503bf88259 100644 --- a/src/state/jump-to-next-index/in-foreign-list.js +++ b/src/state/jump-to-next-index/in-foreign-list.js @@ -92,7 +92,7 @@ export default ({ }; return { - center: newCenter, + pageCenter: newCenter, impact: newImpact, }; }; diff --git a/src/state/jump-to-next-index/in-home-list.js b/src/state/jump-to-next-index/in-home-list.js index 0c6892f84e..5089f4830b 100644 --- a/src/state/jump-to-next-index/in-home-list.js +++ b/src/state/jump-to-next-index/in-home-list.js @@ -105,7 +105,7 @@ export default ({ }; const result: Result = { - center: newCenter, + pageCenter: newCenter, impact: newImpact, }; diff --git a/src/state/jump-to-next-index/jump-to-next-index-types.js b/src/state/jump-to-next-index/jump-to-next-index-types.js index 67709e00b5..c7f11857e7 100644 --- a/src/state/jump-to-next-index/jump-to-next-index-types.js +++ b/src/state/jump-to-next-index/jump-to-next-index-types.js @@ -16,6 +16,8 @@ export type Args = {| |} export type Result = {| - center: Position, + // the new page center position of the element + pageCenter: Position, + // the impact of the movement impact: DragImpact, |} diff --git a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js index 698aa5f52c..83e25dc82d 100644 --- a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js +++ b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js @@ -26,7 +26,7 @@ const getCorners = (droppable: DroppableDimension): Position[] => { type GetBestDroppableArgs = {| isMovingForward: boolean, // the current position of the dragging item - center: Position, + pageCenter: Position, // the home of the draggable source: DroppableDimension, // all the droppables in the system @@ -35,7 +35,7 @@ type GetBestDroppableArgs = {| export default ({ isMovingForward, - center, + pageCenter, source, droppables, }: GetBestDroppableArgs): ?DroppableDimension => { @@ -111,7 +111,7 @@ export default ({ droppable.page.withMargin[axis.start], droppable.page.withMargin[axis.end] ); - return isWithinDroppable(center[axis.line]); + return isWithinDroppable(pageCenter[axis.line]); }); if (contains.length === 1) { @@ -131,8 +131,8 @@ export default ({ // 1. Find the candidate that has the closest corner // 2. If there is a tie - choose the one that is first on the main axis return candidates.sort((a: DroppableDimension, b: DroppableDimension) => { - const first = closest(center, getCorners(a)); - const second = closest(center, getCorners(b)); + const first = closest(pageCenter, getCorners(a)); + const second = closest(pageCenter, getCorners(b)); // if the distances are not equal - choose the shortest if (first !== second) { diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index 4b3b6bb341..3e35e86de2 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -11,7 +11,7 @@ import type { type Args = {| axis: Axis, - center: Position, + pageCenter: Position, // how far the destination Droppable is scrolled scrollOffset: Position, // the droppable that is being moved to @@ -24,7 +24,7 @@ const isVisible = (draggable: DraggableDimension, droppable: DroppableDimension) export default ({ axis, - center, + pageCenter, scrollOffset, destination, draggables, @@ -40,8 +40,8 @@ export default ({ .sort((a: DraggableDimension, b: DraggableDimension): number => { // TODO: not considering scroll offset console.log('scroll offset', scrollOffset); - const distanceToA = distance(center, a.page.withMargin.center); - const distanceToB = distance(center, b.page.withMargin.center); + const distanceToA = distance(pageCenter, a.page.withMargin.center); + const distanceToB = distance(pageCenter, b.page.withMargin.center); // if a is closer - return a if (distanceToA < distanceToB) { diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index cd8893603e..12ed9477ab 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -17,8 +17,8 @@ import type { type Args = {| isMovingForward: boolean, - // the current center of the dragging item - center: Position, + // the current page center of the dragging item + pageCenter: Position, // the dragging item draggableId: DraggableId, // the droppable the dragging item is in @@ -34,7 +34,7 @@ type Args = {| export default ({ isMovingForward, - center, + pageCenter, draggableId, droppableId, home, @@ -47,7 +47,7 @@ export default ({ const destination: ?DroppableDimension = getBestCrossAxisDroppable({ isMovingForward, - center, + pageCenter, source, droppables, }); @@ -64,14 +64,14 @@ export default ({ const target: ?DraggableDimension = getClosestDraggable({ axis: destination.axis, - center, + pageCenter, scrollOffset: destination.scroll.current, destination, draggables, }); return moveToNewDroppable({ - center, + pageCenter, draggable, target, source, diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/index.js b/src/state/move-to-best-droppable/move-to-new-droppable/index.js index 3b26f1f955..7df13093f0 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/index.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/index.js @@ -15,7 +15,7 @@ import type { type Args = {| // the current center position of the draggable - center: Position, + pageCenter: Position, // the draggable that is dragging and needs to move draggable: DraggableDimension, // what the draggable is moving towards @@ -32,7 +32,7 @@ type Args = {| |} export default ({ - center, + pageCenter, destination, draggable, target, @@ -48,7 +48,7 @@ export default ({ ); const isGoingBeforeTarget: boolean = Boolean(target && - center[destination.axis.line] < target.page.withMargin.center[destination.axis.line]); + pageCenter[destination.axis.line] < target.page.withMargin.center[destination.axis.line]); // moving back to the home list if (destination.id === draggable.droppableId) { diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js b/src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js index ae6d9079ed..c7f0f0d95c 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js @@ -3,7 +3,7 @@ import type { Position, DragImpact } from '../../../types'; export type Result = {| // how far the draggable needs to move to be in its new home - center: Position, + pageCenter: Position, // The impact of the movement impact: DragImpact, |} diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js index 93d346f712..9922902969 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js @@ -58,7 +58,7 @@ export default ({ }; return { - center: newCenter, + pageCenter: newCenter, impact: newImpact, }; } @@ -105,7 +105,7 @@ export default ({ }; return { - center: newCenter, + pageCenter: newCenter, impact: newImpact, }; }; diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js index d851f7bacc..710c61b13c 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js @@ -61,7 +61,7 @@ export default ({ }; return { - center: newCenter, + pageCenter: newCenter, impact: newImpact, }; } @@ -105,7 +105,7 @@ export default ({ }; return { - center: newCenter, + pageCenter: newCenter, impact: newImpact, }; }; diff --git a/src/state/reducer.js b/src/state/reducer.js index d4c3390946..d5809fd4b9 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -427,11 +427,14 @@ export default (state: State = clean('IDLE'), action: Action): State => { // return state; // } + const page: Position = result.pageCenter; + const client: Position = subtract(page, existing.current.windowScroll); + return move({ state, impact, - clientSelection: result.center, - pageSelection: result.center, + clientSelection: client, + pageSelection: page, shouldAnimate: true, }); } @@ -462,7 +465,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { const result: ?MoveToNewDroppable = moveToBestDroppable({ isMovingForward: action.type === 'CROSS_AXIS_MOVE_FORWARD', - center, + pageCenter: center, draggableId, droppableId, home, @@ -475,10 +478,13 @@ export default (state: State = clean('IDLE'), action: Action): State => { return state; } + const page: Position = result.pageCenter; + const client: Position = subtract(page, current.windowScroll); + return move({ state, - clientSelection: result.center, - pageSelection: result.center, + clientSelection: client, + pageSelection: page, impact: result.impact, shouldAnimate: true, }); diff --git a/src/types.js b/src/types.js index 80ce685b93..ed00797e09 100644 --- a/src/types.js +++ b/src/types.js @@ -54,7 +54,7 @@ export type DimensionFragment = {| export type DraggableDimension = {| id: DraggableId, droppableId: DroppableId, - // relative to the current viewport + // relative to the viewport when the drag started client: {| withMargin: DimensionFragment, withoutMargin: DimensionFragment, @@ -119,9 +119,9 @@ export type WithinDroppable = {| export type InitialDrag = {| source: DraggableLocation, - // viewport + // relative to the viewport when the drag started client: InitialDragLocation, - // viewport + window scroll + // viewport + window scroll (position relative to 0, 0) page: InitialDragLocation, // Storing scroll directly to support movement during a window scroll. // Value required for comparison with current scroll From 434abf5a3d4828a3bfe5286442fbc3cb0cdd76ce Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 10:12:32 +1000 Subject: [PATCH 056/117] fixing incorrect impact order when moving back to home list when moving before home location --- .../move-to-new-droppable/to-home-list.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js index 710c61b13c..d1d21fd2ff 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js @@ -80,15 +80,12 @@ export default ({ const needsToMove: DraggableId[] = (() => { if (isMovingBeyondHome) { - return insideDroppable.slice(originalIndex + 1, targetIndex + 1); + // need to ensure that the list is sorted with the closest item being first + return insideDroppable.slice(originalIndex + 1, targetIndex + 1).reverse(); } return insideDroppable.slice(targetIndex, originalIndex); })() - .map((d: DraggableDimension): DraggableId => d.id) - // need to ensure that the list is sorted with the closest item being first - .reverse(); - - console.log('to home list moved', needsToMove); + .map((d: DraggableDimension): DraggableId => d.id); const newImpact: DragImpact = { movement: { From 6f3cd0618c2041953f9058bc01424ffe6e1f916a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 11:08:06 +1000 Subject: [PATCH 057/117] adding isEnabled flag --- src/state/move-to-best-droppable/index.js | 1 - .../move-to-new-droppable/to-home-list.js | 1 + src/types.js | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index 12ed9477ab..f95a835250 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -74,7 +74,6 @@ export default ({ pageCenter, draggable, target, - source, destination, home, impact, diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js index d1d21fd2ff..29ed7141de 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js @@ -79,6 +79,7 @@ export default ({ }); const needsToMove: DraggableId[] = (() => { + // TODO: explain the index trickery if (isMovingBeyondHome) { // need to ensure that the list is sorted with the closest item being first return insideDroppable.slice(originalIndex + 1, targetIndex + 1).reverse(); diff --git a/src/types.js b/src/types.js index ed00797e09..2ac8f1c8d5 100644 --- a/src/types.js +++ b/src/types.js @@ -69,6 +69,7 @@ export type DraggableDimension = {| export type DroppableDimension = {| id: DroppableId, axis: Axis, + isEnabled: boolean, scroll: {| initial: Position, current: Position, @@ -94,7 +95,7 @@ export type DroppableDimensionMap = { [key: DroppableId]: DroppableDimension }; export type DragMovement = {| // The draggables that need to move in response to a drag. - // Ordered by the closest to the start of the droppable + // Ordered by closest draggable to the *current* location of the dragging item draggables: DraggableId[], amount: Position, // is moving forward relative to the starting position From e218a0dcfef495600065054e50c0867d3a505297 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 14:16:46 +1000 Subject: [PATCH 058/117] new client offset now works correctly for keyboard dragging --- src/state/get-drag-impact.js | 5 ++++- src/state/get-new-home-client-offset.js | 15 ++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 3476866b9f..b2deb14005 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -123,6 +123,9 @@ export default ({ }) .map((dimension: DraggableDimension): DroppableId => dimension.id); + // Need to ensure that we always order by the closest impacted item + const ordered: DraggableId[] = isBeyondStartPosition ? moved.reverse() : moved; + const startIndex = insideDroppable.indexOf(draggingDimension); const index: number = (() => { if (!isWithinHomeDroppable) { @@ -149,7 +152,7 @@ export default ({ const movement: DragMovement = { amount, - draggables: moved, + draggables: ordered, isBeyondStartPosition: shouldDisplaceItemsForward, }; diff --git a/src/state/get-new-home-client-offset.js b/src/state/get-new-home-client-offset.js index 7c7c484960..260c6161b9 100644 --- a/src/state/get-new-home-client-offset.js +++ b/src/state/get-new-home-client-offset.js @@ -55,9 +55,8 @@ export default ({ } = destinationDroppable; // All the draggables in the destination (even the ones that haven't moved) - const draggablesInDestination: DraggableDimension[] = draggableMapToList(draggables).filter( - draggable => draggable.droppableId === destinationDroppableId - ); + const draggablesInDestination: DraggableDimension[] = draggableMapToList(draggables) + .filter(draggable => draggable.droppableId === destinationDroppableId); // The dimension of the item being dragged const draggedDimensionFragment: DimensionFragment = draggedItem.client.withMargin; @@ -66,12 +65,10 @@ export default ({ const destinationDimensionFragment: DimensionFragment = (() => { // If we're not dragging into an empty list if (movedDraggables.length) { - // The index of the last item being displaced - const displacedIndex: number = isBeyondStartPosition ? movedDraggables.length - 1 : 0; - // Return the dimension of the last item being displaced - return draggables[ - movedDraggables[displacedIndex] - ].client.withMargin; + // Return the dimension of the closest item being displaced + // The moved list is ordered by the closest to the furthest + // so we can just grab the first moved item. + return draggables[movedDraggables[0]].client.withMargin; } // If we're dragging to the last place in a new droppable From 7d6a2c8eafcaa0a58ec3685d5d64a060c3d189bd Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 14:40:27 +1000 Subject: [PATCH 059/117] fixing mouse movement --- src/state/get-drag-impact.js | 9 +- src/state/jump-to-next-index.old.js | 271 --------------------- test/unit/state/jump-to-next-index.spec.js | 12 +- 3 files changed, 18 insertions(+), 274 deletions(-) delete mode 100644 src/state/jump-to-next-index.old.js diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index b2deb14005..5117bfc6f3 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -124,7 +124,12 @@ export default ({ .map((dimension: DraggableDimension): DroppableId => dimension.id); // Need to ensure that we always order by the closest impacted item - const ordered: DraggableId[] = isBeyondStartPosition ? moved.reverse() : moved; + const ordered: DraggableId[] = (() => { + if (!isWithinHomeDroppable) { + return moved; + } + return isBeyondStartPosition ? moved.reverse() : moved; + })(); const startIndex = insideDroppable.indexOf(draggingDimension); const index: number = (() => { @@ -156,7 +161,7 @@ export default ({ isBeyondStartPosition: shouldDisplaceItemsForward, }; - console.log('mouse moved', moved); + console.log('mouse moved', ordered); const impact: DragImpact = { movement, diff --git a/src/state/jump-to-next-index.old.js b/src/state/jump-to-next-index.old.js deleted file mode 100644 index 2d3129ac07..0000000000 --- a/src/state/jump-to-next-index.old.js +++ /dev/null @@ -1,271 +0,0 @@ -// @flow -import memoizeOne from 'memoize-one'; -import getDraggablesInsideDroppable from './get-draggables-inside-droppable'; -import { patch } from './position'; -import moveToEdge from './move-to-edge'; -import type { Edge } from './move-to-edge'; -import type { - DraggableLocation, - DraggableDimension, - DroppableDimension, - DraggableDimensionMap, - DroppableDimensionMap, - Position, - DraggableId, - Axis, - DragImpact, - DimensionFragment, -} from '../types'; - -const getIndex = memoizeOne( - (draggables: DraggableDimension[], - target: DraggableDimension - ): number => draggables.indexOf(target) -); - -type JumpToNextArgs = {| - isMovingForward: boolean, - draggableId: DraggableId, - impact: DragImpact, - droppable: DroppableDimension, - draggables: DraggableDimensionMap, -|} - -export type JumpToNextResult = {| - center: Position, - impact: DragImpact, -|} -// const pull = - -// const pull: ShiftPosition = shift(subtract, size: number); -// const push: ShiftPosition = shift(add, size: number); - -export default ({ - isMovingForward, - draggableId, - impact, - droppable, - draggables, - }: JumpToNextArgs): ?JumpToNextResult => { - if (!impact.destination) { - console.error('cannot move forward when there is not previous destination'); - return null; - } - - const location: DraggableLocation = impact.destination; - const draggable: DraggableDimension = draggables[draggableId]; - const axis: Axis = droppable.axis; - - const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( - droppable, - draggables, - ); - - // has the dragging item moved into a new list? - const isInHomeList: boolean = draggable.droppableId === droppable.id; - - if (!isInHomeList) { - const currentIndex: number = location.index; - const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; - const startIndex: number = impact.foreignDestinationStartIndex; - - // cannot move forward beyond the last item - if (proposedIndex > insideDroppable.length) { - return null; - } - - // cannot move before the first item - if (proposedIndex < 0) { - return null; - } - - console.log('foreignDestinationStartIndex', startIndex); - - const destinationIndex = proposedIndex >= startIndex ? proposedIndex - 1 : proposedIndex - 1; - - console.log('destinationIndex', destinationIndex); - - // the index's need to be adjusted to take into account the additional element - // 'inserted' at start position - const destination: DimensionFragment = (() => { - if (proposedIndex === 0) { - console.log('destination id', insideDroppable[0].id); - return insideDroppable[0].page.withMargin; - } - - console.log('destination id', insideDroppable[destinationIndex].id); - return insideDroppable[destinationIndex].page.withMargin; - })(); - const atProposedIndex: DraggableDimension = insideDroppable[proposedIndex]; - - // const sourceEdge = isMovingForward ? 'start' : 'end'; - - // const destinationEdge = (() => { - // if (isMovingToEnd) { - // return 'end'; - // } - - // return isMovingForward ? 'start' : 'end'; - // })(); - - const isMovingTowardStart = (isMovingForward && proposedIndex <= startIndex) || - (!isMovingForward && proposedIndex >= startIndex); - - const sourceEdge = (() => { - if (proposedIndex === 0) { - console.log('returning custom edge'); - return 'start'; - } - - // is moving away from the start - if (!isMovingTowardStart) { - return isMovingForward ? 'start' : 'start'; - } - // is moving back towards the start - return isMovingForward ? 'start' : 'start'; - })(); - - const destinationEdge = (() => { - if (proposedIndex === 0) { - console.log('returning custom edge'); - return 'start'; - } - - // is moving away from the start - if (!isMovingTowardStart) { - return isMovingForward ? 'end' : 'end'; - } - // is moving back towards the start - return isMovingForward ? 'end' : 'end'; - })(); - - console.log('sourceEdge:', sourceEdge); - console.log('destinationEdge:', destinationEdge); - // console.log('edge', edge); - // console.log('is moving forward', isMovingForward); - // console.log('isMovingTowardStart', isMovingTowardStart); - - const newCenter: Position = moveToEdge({ - source: draggable.page.withoutMargin, - sourceEdge, - destination, - destinationEdge, - destinationAxis: droppable.axis, - }); - - const moved: DraggableId[] = (() => { - if (!isMovingForward) { - // need to trim the existing movement - return [atProposedIndex.id, ...impact.movement.draggables]; - } - return impact.movement.draggables.slice(1, impact.movement.draggables.length); - })(); - - console.log('moved', moved); - - const newImpact: DragImpact = { - movement: { - draggables: moved, - // The amount of movement will always be the size of the dragging item - amount: patch(axis.line, draggable.page.withMargin[axis.size]), - // when in another list we are never past the start position - isBeyondStartPosition: false, - }, - destination: { - droppableId: droppable.id, - index: proposedIndex, - }, - direction: droppable.axis.direction, - foreignDestinationStartIndex: impact.foreignDestinationStartIndex, - }; - - // console.log('returning result', { newCenter, newImpact }); - - return { - center: newCenter, - impact: newImpact, - }; - } - - // even if not in home list - - - // If not in home list - need to insert draggable into correct position in list - // const tempIndex: number = impact.destination.index; - - console.log('moving when in home list'); - - const startIndex: number = getIndex(insideDroppable, draggable); - const currentIndex: number = location.index; - const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; - - if (startIndex === -1) { - console.error('could not find draggable inside current droppable'); - return null; - } - - // cannot move forward beyond the last item - if (proposedIndex > insideDroppable.length - 1) { - return null; - } - - // cannot move before the first item - if (proposedIndex < 0) { - return null; - } - - const destination: DraggableDimension = insideDroppable[proposedIndex]; - const isMovingTowardStart = (isMovingForward && proposedIndex <= startIndex) || - (!isMovingForward && proposedIndex >= startIndex); - - const edge: Edge = (() => { - // is moving away from the start - if (!isMovingTowardStart) { - return isMovingForward ? 'end' : 'start'; - } - // is moving back towards the start - return isMovingForward ? 'start' : 'end'; - })(); - - const newCenter: Position = moveToEdge({ - source: draggable.page.withoutMargin, - sourceEdge: edge, - destination: destination.page.withoutMargin, - destinationEdge: edge, - destinationAxis: droppable.axis, - }); - - // Calculate DragImpact - - // 1. If moving back towards where we started - // we need to remove the latest addition - // 2. If we are moving away from where we started, - // we need to add the next draggable to the impact - const moved: DraggableId[] = isMovingTowardStart ? - impact.movement.draggables.slice(0, impact.movement.draggables.length - 1) : - [...impact.movement.draggables, destination.id]; - - console.log('moved', moved); - console.log('destination', destination); - - const newImpact: DragImpact = { - movement: { - draggables: moved, - // 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, - }, - destination: { - droppableId: droppable.id, - index: proposedIndex, - }, - direction: droppable.axis.direction, - }; - - const result: JumpToNextResult = { - center: newCenter, - impact: newImpact, - }; - - return result; -}; - diff --git a/test/unit/state/jump-to-next-index.spec.js b/test/unit/state/jump-to-next-index.spec.js index d49f0120e5..212351aded 100644 --- a/test/unit/state/jump-to-next-index.spec.js +++ b/test/unit/state/jump-to-next-index.spec.js @@ -1,6 +1,6 @@ // @flow import jumpToNextIndex from '../../../src/state/jump-to-next-index'; -import type { JumpToNextResult } from '../../../src/state/jump-to-next-index'; +import type { Result } from '../../../src/state/jump-to-next-index/jump-to-next-index-types'; import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; import getClientRect from '../../utils/get-client-rect'; import moveToEdge from '../../../src/state/move-to-edge'; @@ -21,6 +21,16 @@ import type { const droppableId: DroppableId = 'drop-1'; +describe('jump to next index', () => { + describe('in home list', () => { + + }); + + describe('in foreign list', () => { + + }); +}); + describe('jump to next index', () => { [vertical, horizontal].forEach((axis: Axis) => { const droppable: DroppableDimension = getDroppableDimension({ From b3b39a0ca1c63ebb118ac0cc7395ce8a449b216e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 14:52:00 +1000 Subject: [PATCH 060/117] fixing existing jump to next tests --- src/state/jump-to-next-index/in-home-list.js | 6 +- test/unit/state/jump-to-next-index.spec.js | 1024 +++++++++--------- 2 files changed, 512 insertions(+), 518 deletions(-) diff --git a/src/state/jump-to-next-index/in-home-list.js b/src/state/jump-to-next-index/in-home-list.js index 5089f4830b..9f2fbe5bc3 100644 --- a/src/state/jump-to-next-index/in-home-list.js +++ b/src/state/jump-to-next-index/in-home-list.js @@ -27,7 +27,6 @@ export default ({ droppable, draggables, }: Args): ?Result => { - console.log('in-home-list.js'); if (!impact.destination) { console.error('cannot jump to next index when there is not previous destination'); return null; @@ -84,12 +83,13 @@ export default ({ // Calculate DragImpact + // List is sorted where the items closest to where the draggable is currently go first const moved: DraggableId[] = isMovingTowardStart ? + // remove the most recently impacted impact.movement.draggables.slice(1, impact.movement.draggables.length) : + // add the destination as the most recently impacted [destination.id, ...impact.movement.draggables]; - console.log('in home list moved', moved); - const newImpact: DragImpact = { movement: { draggables: moved, diff --git a/test/unit/state/jump-to-next-index.spec.js b/test/unit/state/jump-to-next-index.spec.js index 212351aded..00234d55bc 100644 --- a/test/unit/state/jump-to-next-index.spec.js +++ b/test/unit/state/jump-to-next-index.spec.js @@ -1,5 +1,5 @@ // @flow -import jumpToNextIndex from '../../../src/state/jump-to-next-index'; +import jumpToNextIndex from '../../../src/state/jump-to-next-index/'; import type { Result } from '../../../src/state/jump-to-next-index/jump-to-next-index-types'; import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; import getClientRect from '../../utils/get-client-rect'; @@ -14,7 +14,6 @@ import type { DraggableDimension, DroppableDimension, DraggableDimensionMap, - DroppableDimensionMap, DraggableLocation, Position, } from '../../../src/types'; @@ -23,663 +22,658 @@ const droppableId: DroppableId = 'drop-1'; describe('jump to next index', () => { describe('in home list', () => { + [vertical, horizontal].forEach((axis: Axis) => { + const droppable: DroppableDimension = getDroppableDimension({ + id: droppableId, + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 0, + bottom: 1000, + right: 1000, + }), + }); - }); - - describe('in foreign list', () => { - - }); -}); - -describe('jump to next index', () => { - [vertical, horizontal].forEach((axis: Axis) => { - const droppable: DroppableDimension = getDroppableDimension({ - id: droppableId, - direction: axis.direction, - clientRect: getClientRect({ - top: 0, - left: 0, - bottom: 1000, - right: 1000, - }), - }); - - // size: 100 - const draggable1: DraggableDimension = getDraggableDimension({ - id: 'draggable1', - droppableId, - clientRect: getClientRect({ - top: 0, - left: 0, - bottom: 100, - right: 100, - }), - }); - - // size: 199 - const draggable2: DraggableDimension = getDraggableDimension({ - id: 'draggable2', - droppableId, - clientRect: getClientRect({ - top: 101, - left: 101, - bottom: 300, - right: 300, - }), - }); - - // size: 299 - const draggable3: DraggableDimension = getDraggableDimension({ - id: 'draggable3', - droppableId, - clientRect: getClientRect({ - top: 301, - left: 301, - bottom: 600, - right: 600, - }), - }); + // size: 100 + const draggable1: DraggableDimension = getDraggableDimension({ + id: 'draggable1', + droppableId, + clientRect: getClientRect({ + top: 0, + left: 0, + bottom: 100, + right: 100, + }), + }); - const droppables: DroppableDimensionMap = { - [droppable.id]: droppable, - }; - - const draggables: DraggableDimensionMap = { - [draggable1.id]: draggable1, - [draggable2.id]: draggable2, - [draggable3.id]: draggable3, - }; - describe(`on the ${axis.direction} axis`, () => { - describe('jump forward', () => { - it('should return null if cannot move forward', () => { - const impact: DragImpact = { - movement: { - draggables: [], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - index: 2, - droppableId: droppable.id, - }, - }; - - const result: ?JumpToNextResult = jumpToNextIndex({ - isMovingForward: true, - draggableId: draggable3.id, - impact, - draggables, - droppables, - }); + // size: 199 + const draggable2: DraggableDimension = getDraggableDimension({ + id: 'draggable2', + droppableId, + clientRect: getClientRect({ + top: 101, + left: 101, + bottom: 300, + right: 300, + }), + }); - expect(result).toBe(null); - }); + // size: 299 + const draggable3: DraggableDimension = getDraggableDimension({ + id: 'draggable3', + droppableId, + clientRect: getClientRect({ + top: 301, + left: 301, + bottom: 600, + right: 600, + }), + }); - describe('is moving away from start position', () => { - describe('dragging first item forward one position', () => { - // dragging the first item forward into the second position - const movement: DragMovement = { - draggables: [], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }; - const destination: DraggableLocation = { - index: 0, - droppableId: droppable.id, - }; + const draggables: DraggableDimensionMap = { + [draggable1.id]: draggable1, + [draggable2.id]: draggable2, + [draggable3.id]: draggable3, + }; + describe(`on the ${axis.direction} axis`, () => { + describe('jump forward', () => { + it('should return null if cannot move forward', () => { const impact: DragImpact = { - movement, + movement: { + draggables: [], + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, direction: axis.direction, - destination, + destination: { + index: 2, + droppableId: droppable.id, + }, }; - const result: ?JumpToNextResult = jumpToNextIndex({ + + const result: ?Result = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable1.id, + draggableId: draggable3.id, impact, + droppable, draggables, - droppables, }); - if (!result) { - throw new Error('invalid result'); - } - - it('should move the end of the dragging item to the end of the next item', () => { - const expected: Position = moveToEdge({ - source: draggable1.page.withoutMargin, - sourceEdge: 'end', - destination: draggable2.page.withoutMargin, - destinationEdge: 'end', - destinationAxis: axis, - }); - - expect(result.center).toEqual(expected); - }); + expect(result).toBe(null); + }); - it('should move the item into the second spot and move the second item out of the way', () => { - const expected: DragImpact = { - movement: { - draggables: [draggable2.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: true, - }, + describe('is moving away from start position', () => { + describe('dragging first item forward one position', () => { + // dragging the first item forward into the second position + const movement: DragMovement = { + draggables: [], + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }; + const destination: DraggableLocation = { + index: 0, + droppableId: droppable.id, + }; + const impact: DragImpact = { + movement, direction: axis.direction, - // is now in the second position - destination: { - droppableId: droppable.id, - index: 1, - }, + destination, }; + const result: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: draggable1.id, + impact, + draggables, + droppable, + }); - expect(result.impact).toEqual(expected); - }); - }); + if (!result) { + throw new Error('invalid result'); + } - describe('dragging second item forward one position', () => { - const movement: DragMovement = { - draggables: [], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }; - const destination: DraggableLocation = { - index: 1, - droppableId: droppable.id, - }; - const impact: DragImpact = { - movement, - direction: axis.direction, - destination, - }; - const result: ?JumpToNextResult = jumpToNextIndex({ - isMovingForward: true, - draggableId: draggable2.id, - impact, - draggables, - droppables, - }); + it('should move the end of the dragging item to the end of the next item', () => { + const expected: Position = moveToEdge({ + source: draggable1.page.withoutMargin, + sourceEdge: 'end', + destination: draggable2.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); - if (!result) { - throw new Error('invalid result'); - } - - it('should move the end of the dragging item to the end of the next item', () => { - const expected: Position = moveToEdge({ - source: draggable2.page.withoutMargin, - sourceEdge: 'end', - destination: draggable3.page.withoutMargin, - destinationEdge: 'end', - destinationAxis: axis, + expect(result.pageCenter).toEqual(expected); }); - expect(result.center).toEqual(expected); + it('should move the item into the second spot and move the second item out of the way', () => { + const expected: DragImpact = { + movement: { + draggables: [draggable2.id], + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: true, + }, + direction: axis.direction, + // is now in the second position + destination: { + droppableId: droppable.id, + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should move the dragging item into the third spot and move the third item out of the way', () => { - const expected: DragImpact = { - movement: { - draggables: [draggable3.id], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: true, - }, + describe('dragging second item forward one position', () => { + const movement: DragMovement = { + draggables: [], + amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }; + const destination: DraggableLocation = { + index: 1, + droppableId: droppable.id, + }; + const impact: DragImpact = { + movement, direction: axis.direction, - // is now in the second position - destination: { - droppableId: droppable.id, - index: 2, - }, + destination, }; + const result: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: draggable2.id, + impact, + draggables, + droppable, + }); - expect(result.impact).toEqual(expected); - }); - }); + if (!result) { + throw new Error('invalid result'); + } - describe('dragging first item forward one position after already moving it forward once', () => { - const impact: DragImpact = { - movement: { - // second item has already moved - draggables: [draggable2.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // draggable1 is now in the second position - destination: { - droppableId: droppable.id, - index: 1, - }, - }; - const result: ?JumpToNextResult = jumpToNextIndex({ - isMovingForward: true, - draggableId: draggable1.id, - impact, - draggables, - droppables, - }); + it('should move the end of the dragging item to the end of the next item', () => { + const expected: Position = moveToEdge({ + source: draggable2.page.withoutMargin, + sourceEdge: 'end', + destination: draggable3.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); - if (!result) { - throw new Error('invalid result'); - } - - it('should move the end of the dragging item to the end of the next item', () => { - // next dimension from the current index is draggable3 - const expected: Position = moveToEdge({ - source: draggable1.page.withoutMargin, - sourceEdge: 'end', - destination: draggable3.page.withoutMargin, - destinationEdge: 'end', - destinationAxis: axis, + expect(result.pageCenter).toEqual(expected); }); - expect(result.center).toEqual(expected); + it('should move the dragging item into the third spot and move the third item out of the way', () => { + const expected: DragImpact = { + movement: { + draggables: [draggable3.id], + amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + isBeyondStartPosition: true, + }, + direction: axis.direction, + // is now in the second position + destination: { + droppableId: droppable.id, + index: 2, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should move the third item out of the way', () => { - const expected: DragImpact = { + describe('dragging first item forward one position after already moving it forward once', () => { + const impact: DragImpact = { movement: { - // adding draggable3 to the list - draggables: [draggable2.id, draggable3.id], + // second item has already moved + draggables: [draggable2.id], amount: patch(axis.line, draggable1.page.withMargin[axis.size]), isBeyondStartPosition: true, }, direction: axis.direction, - // is now in the second position + // draggable1 is now in the second position destination: { droppableId: droppable.id, - index: 2, + index: 1, }, }; + const result: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: draggable1.id, + impact, + draggables, + droppable, + }); - expect(result.impact).toEqual(expected); - }); - }); - }); + if (!result) { + throw new Error('invalid result'); + } + + it('should move the end of the dragging item to the end of the next item', () => { + // next dimension from the current index is draggable3 + const expected: Position = moveToEdge({ + source: draggable1.page.withoutMargin, + sourceEdge: 'end', + destination: draggable3.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + + expect(result.pageCenter).toEqual(expected); + }); - describe('is moving toward start position', () => { - describe('dragging item forward to starting position', () => { - // dragging the second item (draggable2), which has previously - // been moved backwards and is now in the first position - const impact: DragImpact = { - movement: { - draggables: [draggable1.id], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - index: 0, - droppableId: droppable.id, - }, - }; - const result: ?JumpToNextResult = jumpToNextIndex({ - isMovingForward: true, - draggableId: draggable2.id, - impact, - draggables, - droppables, - }); + it('should move the third item out of the way', () => { + const expected: DragImpact = { + movement: { + // adding draggable3 to the list + // list is sorted by the the closest to the current item + draggables: [draggable3.id, draggable2.id], + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: true, + }, + direction: axis.direction, + // is now in the second position + destination: { + droppableId: droppable.id, + index: 2, + }, + }; - if (!result) { - throw new Error('invalid result of jumpToNextIndex'); - } - - it('should move the start of the dragging item to the end of the previous item (which its original position)', () => { - const expected: Position = moveToEdge({ - source: draggable2.page.withoutMargin, - sourceEdge: 'start', - destination: draggable2.page.withoutMargin, - destinationEdge: 'start', - destinationAxis: axis, + expect(result.impact).toEqual(expected); }); - - expect(result.center).toEqual(expected); - // is now back at its original position - expect(result.center).toEqual(draggable2.page.withoutMargin.center); }); + }); - it('should return an empty impact', () => { - const expected: DragImpact = { + describe('is moving toward start position', () => { + describe('dragging item forward to starting position', () => { + // dragging the second item (draggable2), which has previously + // been moved backwards and is now in the first position + const impact: DragImpact = { movement: { - draggables: [], + draggables: [draggable1.id], amount: patch(axis.line, draggable2.page.withMargin[axis.size]), isBeyondStartPosition: false, }, + direction: axis.direction, destination: { + index: 0, droppableId: droppable.id, - index: 1, }, - direction: axis.direction, }; + const result: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: draggable2.id, + impact, + draggables, + droppable, + }); - expect(result.impact).toEqual(expected); - }); - }); - - describe('dragging forwards, but not beyond the starting position', () => { - // draggable3 has moved backwards past draggable2 and draggable1 - const impact: DragImpact = { - movement: { - // second and first item have already moved - draggables: [draggable2.id, draggable1.id], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // draggable3 is now in the first position - destination: { - droppableId: droppable.id, - index: 0, - }, - }; - // moving draggable3 forward one position - const result: ?JumpToNextResult = jumpToNextIndex({ - isMovingForward: true, - draggableId: draggable3.id, - impact, - draggables, - droppables, - }); - - if (!result) { - throw new Error('invalid result'); - } - - it('should move to the start of the draggable item to the start position of the destination draggable', () => { - const expected: Position = moveToEdge({ - source: draggable3.page.withoutMargin, - sourceEdge: 'start', - destination: draggable2.page.withoutMargin, - destinationEdge: 'start', - destinationAxis: axis, + if (!result) { + throw new Error('invalid result of jumpToNextIndex'); + } + + it('should move the start of the dragging item to the end of the previous item (which its original position)', () => { + const expected: Position = moveToEdge({ + source: draggable2.page.withoutMargin, + sourceEdge: 'start', + destination: draggable2.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); + + expect(result.pageCenter).toEqual(expected); + // is now back at its original position + expect(result.pageCenter).toEqual(draggable2.page.withoutMargin.center); }); - expect(result.center).toEqual(expected); + it('should return an empty impact', () => { + const expected: DragImpact = { + movement: { + draggables: [], + amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + destination: { + droppableId: droppable.id, + index: 1, + }, + direction: axis.direction, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should remove the first dimension from the impact', () => { - const expected: DragImpact = { + describe('dragging forwards, but not beyond the starting position', () => { + // draggable3 has moved backwards past draggable2 and draggable1 + const impact: DragImpact = { movement: { - draggables: [draggable2.id], + // second and first item have already moved + // sorted by the draggable that is closest to where the dragging item is + draggables: [draggable1.id, draggable2.id], amount: patch(axis.line, draggable3.page.withMargin[axis.size]), - // is still behind where it started - isBeyondStartPosition: false, + isBeyondStartPosition: true, }, direction: axis.direction, - // is now in the second position + // draggable3 is now in the first position destination: { droppableId: droppable.id, - index: 1, + index: 0, }, }; + // moving draggable3 forward one position + const result: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: draggable3.id, + impact, + draggables, + droppable, + }); - expect(result.impact).toEqual(expected); - }); - }); - }); - }); + if (!result) { + throw new Error('invalid result'); + } - describe('jump backward', () => { - it('should return null if cannot move backward', () => { - const impact: DragImpact = { - movement: { - draggables: [], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - index: 0, - droppableId: droppable.id, - }, - }; - - const result: ?JumpToNextResult = jumpToNextIndex({ - isMovingForward: false, - draggableId: draggable1.id, - impact, - draggables, - droppables, - }); + it('should move to the start of the draggable item to the start position of the destination draggable', () => { + const expected: Position = moveToEdge({ + source: draggable3.page.withoutMargin, + sourceEdge: 'start', + destination: draggable2.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); - expect(result).toBe(null); + expect(result.pageCenter).toEqual(expected); + }); + + it('should remove the first dimension from the impact', () => { + const expected: DragImpact = { + movement: { + draggables: [draggable2.id], + amount: patch(axis.line, draggable3.page.withMargin[axis.size]), + // is still behind where it started + isBeyondStartPosition: false, + }, + direction: axis.direction, + // is now in the second position + destination: { + droppableId: droppable.id, + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); + }); + }); }); - describe('is moving away from start position', () => { - describe('dragging the second item back to the first position', () => { - // no impact yet + describe('jump backward', () => { + it('should return null if cannot move backward', () => { const impact: DragImpact = { movement: { draggables: [], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), isBeyondStartPosition: false, }, + direction: axis.direction, destination: { + index: 0, droppableId: droppable.id, - index: 1, }, - direction: axis.direction, }; - const result: ?JumpToNextResult = jumpToNextIndex({ + + const result: ?Result = jumpToNextIndex({ isMovingForward: false, - draggableId: draggable2.id, + draggableId: draggable1.id, impact, draggables, - droppables, + droppable, }); - if (!result) { - throw new Error('invalid result'); - } - - it('should move the start of the draggable to the start of the previous draggable', () => { - const expected: Position = moveToEdge({ - source: draggable2.page.withoutMargin, - sourceEdge: 'start', - destination: draggable1.page.withoutMargin, - destinationEdge: 'start', - destinationAxis: axis, - }); - - expect(result.center).toEqual(expected); - }); + expect(result).toBe(null); + }); - it('should add the first draggable to the drag impact', () => { - const expected: DragImpact = { + describe('is moving away from start position', () => { + describe('dragging the second item back to the first position', () => { + // no impact yet + const impact: DragImpact = { movement: { - draggables: [draggable1.id], + draggables: [], amount: patch(axis.line, draggable2.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { droppableId: droppable.id, - // is now in the first position - index: 0, + index: 1, }, direction: axis.direction, }; + const result: ?Result = jumpToNextIndex({ + isMovingForward: false, + draggableId: draggable2.id, + impact, + draggables, + droppable, + }); - expect(result.impact).toEqual(expected); - }); - }); + if (!result) { + throw new Error('invalid result'); + } - describe('dragging the third item back to the second position', () => { - const impact: DragImpact = { - movement: { - draggables: [], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, - destination: { - droppableId: droppable.id, - index: 2, - }, - direction: axis.direction, - }; - const result: ?JumpToNextResult = jumpToNextIndex({ - isMovingForward: false, - draggableId: draggable3.id, - impact, - draggables, - droppables, - }); + it('should move the start of the draggable to the start of the previous draggable', () => { + const expected: Position = moveToEdge({ + source: draggable2.page.withoutMargin, + sourceEdge: 'start', + destination: draggable1.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); - if (!result) { - throw new Error('invalid result'); - } - - it('should move the start of the draggable to the start of the previous draggable', () => { - const expected: Position = moveToEdge({ - source: draggable3.page.withoutMargin, - sourceEdge: 'start', - destination: draggable2.page.withoutMargin, - destinationEdge: 'start', - destinationAxis: axis, + expect(result.pageCenter).toEqual(expected); }); - expect(result.center).toEqual(expected); + it('should add the first draggable to the drag impact', () => { + const expected: DragImpact = { + movement: { + draggables: [draggable1.id], + amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + destination: { + droppableId: droppable.id, + // is now in the first position + index: 0, + }, + direction: axis.direction, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should add the second draggable to the drag impact', () => { - const expected: DragImpact = { + describe('dragging the third item back to the second position', () => { + const impact: DragImpact = { movement: { - draggables: [draggable2.id], + draggables: [], amount: patch(axis.line, draggable3.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { droppableId: droppable.id, - // is now in the second position - index: 1, + index: 2, }, direction: axis.direction, }; + const result: ?Result = jumpToNextIndex({ + isMovingForward: false, + draggableId: draggable3.id, + impact, + draggables, + droppable, + }); - expect(result.impact).toEqual(expected); - }); - }); - }); + if (!result) { + throw new Error('invalid result'); + } - describe('is moving towards the start position', () => { - describe('moving back to original position', () => { - // dragged the second item (draggable2) forward once, and is now - // moving backwards towards the start again - const impact: DragImpact = { - movement: { - draggables: [draggable3.id], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { - index: 2, - droppableId: droppable.id, - }, - }; - const result: ?JumpToNextResult = jumpToNextIndex({ - isMovingForward: false, - draggableId: draggable2.id, - impact, - draggables, - droppables, - }); + it('should move the start of the draggable to the start of the previous draggable', () => { + const expected: Position = moveToEdge({ + source: draggable3.page.withoutMargin, + sourceEdge: 'start', + destination: draggable2.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); - if (!result) { - throw new Error('invalid result'); - } - - it('should move the end of the draggable to the end of the next draggable (which is its original position)', () => { - const expected: Position = moveToEdge({ - source: draggable2.page.withoutMargin, - sourceEdge: 'end', - // destination is itself as moving back to home - destination: draggable2.page.withoutMargin, - destinationEdge: 'end', - destinationAxis: axis, + expect(result.pageCenter).toEqual(expected); }); - expect(result.center).toEqual(expected); - // moved back to its original position - expect(result.center).toEqual(draggable2.page.withoutMargin.center); + it('should add the second draggable to the drag impact', () => { + const expected: DragImpact = { + movement: { + draggables: [draggable2.id], + amount: patch(axis.line, draggable3.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + destination: { + droppableId: droppable.id, + // is now in the second position + index: 1, + }, + direction: axis.direction, + }; + + expect(result.impact).toEqual(expected); + }); }); + }); - it('should return an empty impact', () => { - const expected: DragImpact = { + describe('is moving towards the start position', () => { + describe('moving back to original position', () => { + // dragged the second item (draggable2) forward once, and is now + // moving backwards towards the start again + const impact: DragImpact = { movement: { - draggables: [], + draggables: [draggable3.id], amount: patch(axis.line, draggable2.page.withMargin[axis.size]), - isBeyondStartPosition: false, + isBeyondStartPosition: true, }, + direction: axis.direction, destination: { + index: 2, droppableId: droppable.id, - index: 1, }, - direction: axis.direction, }; + const result: ?Result = jumpToNextIndex({ + isMovingForward: false, + draggableId: draggable2.id, + impact, + draggables, + droppable, + }); - expect(result.impact).toEqual(expected); - }); - }); - - describe('moving back, but not far enough to be at the start yet', () => { - // dragged the first item: - // forward twice so it is in the third position - // then moving backward so it is in the second position - const impact: DragImpact = { - movement: { - draggables: [draggable2.id, draggable3.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { - index: 2, - droppableId: droppable.id, - }, - }; - const result: ?JumpToNextResult = jumpToNextIndex({ - isMovingForward: false, - draggableId: draggable1.id, - impact, - draggables, - droppables, - }); - - if (!result) { - throw new Error('invalid result'); - } - - it('should move the end of the draggable to the end of the previous draggable', () => { - const expected: Position = moveToEdge({ - source: draggable1.page.withoutMargin, - sourceEdge: 'end', - destination: draggable2.page.withoutMargin, - destinationEdge: 'end', - destinationAxis: axis, + if (!result) { + throw new Error('invalid result'); + } + + it('should move the end of the draggable to the end of the next draggable (which is its original position)', () => { + const expected: Position = moveToEdge({ + source: draggable2.page.withoutMargin, + sourceEdge: 'end', + // destination is itself as moving back to home + destination: draggable2.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + + expect(result.pageCenter).toEqual(expected); + // moved back to its original position + expect(result.pageCenter).toEqual(draggable2.page.withoutMargin.center); }); - expect(result.center).toEqual(expected); + it('should return an empty impact', () => { + const expected: DragImpact = { + movement: { + draggables: [], + amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + destination: { + droppableId: droppable.id, + index: 1, + }, + direction: axis.direction, + }; + + expect(result.impact).toEqual(expected); + }); }); - it('should remove the third draggable from the drag impact', () => { - const expected: DragImpact = { + describe('moving back, but not far enough to be at the start yet', () => { + // dragged the first item: + // forward twice so it is in the third position + // then moving backward so it is in the second position + const impact: DragImpact = { movement: { - // draggable3 has been removed - draggables: [draggable2.id], + // sorted by closest to where the draggable currently is + draggables: [draggable3.id, draggable2.id], amount: patch(axis.line, draggable1.page.withMargin[axis.size]), isBeyondStartPosition: true, }, + direction: axis.direction, destination: { + index: 2, droppableId: droppable.id, - index: 1, }, - direction: axis.direction, }; + const result: ?Result = jumpToNextIndex({ + isMovingForward: false, + draggableId: draggable1.id, + impact, + draggables, + droppable, + }); + + if (!result) { + throw new Error('invalid result'); + } - expect(result.impact).toEqual(expected); + it('should move the end of the draggable to the end of the previous draggable', () => { + const expected: Position = moveToEdge({ + source: draggable1.page.withoutMargin, + sourceEdge: 'end', + destination: draggable2.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + + expect(result.pageCenter).toEqual(expected); + }); + + it('should remove the third draggable from the drag impact', () => { + const expected: DragImpact = { + movement: { + // draggable3 has been removed + draggables: [draggable2.id], + amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + isBeyondStartPosition: true, + }, + destination: { + droppableId: droppable.id, + index: 1, + }, + direction: axis.direction, + }; + + expect(result.impact).toEqual(expected); + }); }); }); }); }); }); }); + + describe('in foreign list', () => { + + }); }); From 8c0e972e93877fe1173e0d0944aa68cbbbb6d5bd Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 16:18:01 +1000 Subject: [PATCH 061/117] improving tests --- .../jump-to-next-index/in-foreign-list.js | 1 - test/unit/state/jump-to-next-index.spec.js | 530 ++++++++++++++---- 2 files changed, 414 insertions(+), 117 deletions(-) diff --git a/src/state/jump-to-next-index/in-foreign-list.js b/src/state/jump-to-next-index/in-foreign-list.js index 503bf88259..44ea3194fe 100644 --- a/src/state/jump-to-next-index/in-foreign-list.js +++ b/src/state/jump-to-next-index/in-foreign-list.js @@ -20,7 +20,6 @@ export default ({ droppable, draggables, }: Args): ?Result => { - console.log('in-foreign-list.js'); if (!impact.destination) { console.error('cannot jump to next index when there is not previous destination'); return null; diff --git a/test/unit/state/jump-to-next-index.spec.js b/test/unit/state/jump-to-next-index.spec.js index 00234d55bc..cfb29ead8d 100644 --- a/test/unit/state/jump-to-next-index.spec.js +++ b/test/unit/state/jump-to-next-index.spec.js @@ -18,13 +18,11 @@ import type { Position, } from '../../../src/types'; -const droppableId: DroppableId = 'drop-1'; - describe('jump to next index', () => { - describe('in home list', () => { - [vertical, horizontal].forEach((axis: Axis) => { - const droppable: DroppableDimension = getDroppableDimension({ - id: droppableId, + [vertical, horizontal].forEach((axis: Axis) => { + describe(`on the ${axis.direction} axis`, () => { + const home: DroppableDimension = getDroppableDimension({ + id: 'home', direction: axis.direction, clientRect: getClientRect({ top: 0, @@ -35,9 +33,9 @@ describe('jump to next index', () => { }); // size: 100 - const draggable1: DraggableDimension = getDraggableDimension({ - id: 'draggable1', - droppableId, + const home1: DraggableDimension = getDraggableDimension({ + id: 'home1', + droppableId: home.id, clientRect: getClientRect({ top: 0, left: 0, @@ -47,9 +45,9 @@ describe('jump to next index', () => { }); // size: 199 - const draggable2: DraggableDimension = getDraggableDimension({ - id: 'draggable2', - droppableId, + const home2: DraggableDimension = getDraggableDimension({ + id: 'home2', + droppableId: home.id, clientRect: getClientRect({ top: 101, left: 101, @@ -59,9 +57,9 @@ describe('jump to next index', () => { }); // size: 299 - const draggable3: DraggableDimension = getDraggableDimension({ - id: 'draggable3', - droppableId, + const home3: DraggableDimension = getDraggableDimension({ + id: 'home3', + droppableId: home.id, clientRect: getClientRect({ top: 301, left: 301, @@ -70,32 +68,84 @@ describe('jump to next index', () => { }), }); + // foreign droppable + const foreign: DroppableDimension = getDroppableDimension({ + id: 'foreign', + direction: axis.direction, + clientRect: getClientRect({ + top: 1001, + left: 1001, + bottom: 2000, + right: 2000, + }), + }); + + // size: 99 + const foreign1: DraggableDimension = getDraggableDimension({ + id: 'foreign1', + droppableId: foreign.id, + clientRect: getClientRect({ + top: 1001, + left: 1001, + bottom: 1100, + right: 1100, + }), + }); + + // size: 199 + const foreign2: DraggableDimension = getDraggableDimension({ + id: 'foreign2', + droppableId: foreign.id, + clientRect: getClientRect({ + top: 1101, + left: 1101, + bottom: 1300, + right: 1300, + }), + }); + + // size: 299 + const foreign3: DraggableDimension = getDraggableDimension({ + id: 'foreign3', + droppableId: foreign.id, + clientRect: getClientRect({ + top: 1301, + left: 1301, + bottom: 1600, + right: 1600, + }), + }); + const draggables: DraggableDimensionMap = { - [draggable1.id]: draggable1, - [draggable2.id]: draggable2, - [draggable3.id]: draggable3, + [home1.id]: home1, + [home2.id]: home2, + [home3.id]: home3, + [foreign1.id]: foreign1, + [foreign2.id]: foreign2, + [foreign3.id]: foreign3, }; - describe(`on the ${axis.direction} axis`, () => { + + describe('in home list', () => { describe('jump forward', () => { it('should return null if cannot move forward', () => { const impact: DragImpact = { movement: { draggables: [], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: false, }, direction: axis.direction, destination: { index: 2, - droppableId: droppable.id, + droppableId: home.id, }, }; const result: ?Result = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable3.id, + draggableId: home3.id, impact, - droppable, + droppable: home, draggables, }); @@ -107,12 +157,12 @@ describe('jump to next index', () => { // dragging the first item forward into the second position const movement: DragMovement = { draggables: [], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: false, }; const destination: DraggableLocation = { index: 0, - droppableId: droppable.id, + droppableId: home.id, }; const impact: DragImpact = { movement, @@ -121,10 +171,10 @@ describe('jump to next index', () => { }; const result: ?Result = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable1.id, + draggableId: home1.id, impact, draggables, - droppable, + droppable: home, }); if (!result) { @@ -133,9 +183,9 @@ describe('jump to next index', () => { it('should move the end of the dragging item to the end of the next item', () => { const expected: Position = moveToEdge({ - source: draggable1.page.withoutMargin, + source: home1.page.withoutMargin, sourceEdge: 'end', - destination: draggable2.page.withoutMargin, + destination: home2.page.withoutMargin, destinationEdge: 'end', destinationAxis: axis, }); @@ -146,14 +196,14 @@ describe('jump to next index', () => { it('should move the item into the second spot and move the second item out of the way', () => { const expected: DragImpact = { movement: { - draggables: [draggable2.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + draggables: [home2.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: true, }, direction: axis.direction, // is now in the second position destination: { - droppableId: droppable.id, + droppableId: home.id, index: 1, }, }; @@ -165,12 +215,12 @@ describe('jump to next index', () => { describe('dragging second item forward one position', () => { const movement: DragMovement = { draggables: [], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + amount: patch(axis.line, home2.page.withMargin[axis.size]), isBeyondStartPosition: false, }; const destination: DraggableLocation = { index: 1, - droppableId: droppable.id, + droppableId: home.id, }; const impact: DragImpact = { movement, @@ -179,10 +229,10 @@ describe('jump to next index', () => { }; const result: ?Result = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable2.id, + draggableId: home2.id, impact, draggables, - droppable, + droppable: home, }); if (!result) { @@ -191,9 +241,9 @@ describe('jump to next index', () => { it('should move the end of the dragging item to the end of the next item', () => { const expected: Position = moveToEdge({ - source: draggable2.page.withoutMargin, + source: home2.page.withoutMargin, sourceEdge: 'end', - destination: draggable3.page.withoutMargin, + destination: home3.page.withoutMargin, destinationEdge: 'end', destinationAxis: axis, }); @@ -204,14 +254,14 @@ describe('jump to next index', () => { it('should move the dragging item into the third spot and move the third item out of the way', () => { const expected: DragImpact = { movement: { - draggables: [draggable3.id], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + draggables: [home3.id], + amount: patch(axis.line, home2.page.withMargin[axis.size]), isBeyondStartPosition: true, }, direction: axis.direction, // is now in the second position destination: { - droppableId: droppable.id, + droppableId: home.id, index: 2, }, }; @@ -224,23 +274,23 @@ describe('jump to next index', () => { const impact: DragImpact = { movement: { // second item has already moved - draggables: [draggable2.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + draggables: [home2.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: true, }, direction: axis.direction, // draggable1 is now in the second position destination: { - droppableId: droppable.id, + droppableId: home.id, index: 1, }, }; const result: ?Result = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable1.id, + draggableId: home1.id, impact, draggables, - droppable, + droppable: home, }); if (!result) { @@ -250,9 +300,9 @@ describe('jump to next index', () => { it('should move the end of the dragging item to the end of the next item', () => { // next dimension from the current index is draggable3 const expected: Position = moveToEdge({ - source: draggable1.page.withoutMargin, + source: home1.page.withoutMargin, sourceEdge: 'end', - destination: draggable3.page.withoutMargin, + destination: home3.page.withoutMargin, destinationEdge: 'end', destinationAxis: axis, }); @@ -265,14 +315,14 @@ describe('jump to next index', () => { movement: { // adding draggable3 to the list // list is sorted by the the closest to the current item - draggables: [draggable3.id, draggable2.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + draggables: [home3.id, home2.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: true, }, direction: axis.direction, // is now in the second position destination: { - droppableId: droppable.id, + droppableId: home.id, index: 2, }, }; @@ -288,22 +338,22 @@ describe('jump to next index', () => { // been moved backwards and is now in the first position const impact: DragImpact = { movement: { - draggables: [draggable1.id], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + draggables: [home1.id], + amount: patch(axis.line, home2.page.withMargin[axis.size]), isBeyondStartPosition: false, }, direction: axis.direction, destination: { index: 0, - droppableId: droppable.id, + droppableId: home.id, }, }; const result: ?Result = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable2.id, + draggableId: home2.id, impact, draggables, - droppable, + droppable: home, }); if (!result) { @@ -312,27 +362,27 @@ describe('jump to next index', () => { it('should move the start of the dragging item to the end of the previous item (which its original position)', () => { const expected: Position = moveToEdge({ - source: draggable2.page.withoutMargin, + source: home2.page.withoutMargin, sourceEdge: 'start', - destination: draggable2.page.withoutMargin, + destination: home2.page.withoutMargin, destinationEdge: 'start', destinationAxis: axis, }); expect(result.pageCenter).toEqual(expected); // is now back at its original position - expect(result.pageCenter).toEqual(draggable2.page.withoutMargin.center); + expect(result.pageCenter).toEqual(home2.page.withoutMargin.center); }); it('should return an empty impact', () => { const expected: DragImpact = { movement: { draggables: [], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + amount: patch(axis.line, home2.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { - droppableId: droppable.id, + droppableId: home.id, index: 1, }, direction: axis.direction, @@ -348,24 +398,24 @@ describe('jump to next index', () => { movement: { // second and first item have already moved // sorted by the draggable that is closest to where the dragging item is - draggables: [draggable1.id, draggable2.id], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), + draggables: [home1.id, home2.id], + amount: patch(axis.line, home3.page.withMargin[axis.size]), isBeyondStartPosition: true, }, direction: axis.direction, // draggable3 is now in the first position destination: { - droppableId: droppable.id, + droppableId: home.id, index: 0, }, }; // moving draggable3 forward one position const result: ?Result = jumpToNextIndex({ isMovingForward: true, - draggableId: draggable3.id, + draggableId: home3.id, impact, draggables, - droppable, + droppable: home, }); if (!result) { @@ -374,9 +424,9 @@ describe('jump to next index', () => { it('should move to the start of the draggable item to the start position of the destination draggable', () => { const expected: Position = moveToEdge({ - source: draggable3.page.withoutMargin, + source: home3.page.withoutMargin, sourceEdge: 'start', - destination: draggable2.page.withoutMargin, + destination: home2.page.withoutMargin, destinationEdge: 'start', destinationAxis: axis, }); @@ -387,15 +437,15 @@ describe('jump to next index', () => { it('should remove the first dimension from the impact', () => { const expected: DragImpact = { movement: { - draggables: [draggable2.id], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), + draggables: [home2.id], + amount: patch(axis.line, home3.page.withMargin[axis.size]), // is still behind where it started isBeyondStartPosition: false, }, direction: axis.direction, // is now in the second position destination: { - droppableId: droppable.id, + droppableId: home.id, index: 1, }, }; @@ -411,22 +461,22 @@ describe('jump to next index', () => { const impact: DragImpact = { movement: { draggables: [], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: false, }, direction: axis.direction, destination: { index: 0, - droppableId: droppable.id, + droppableId: home.id, }, }; const result: ?Result = jumpToNextIndex({ isMovingForward: false, - draggableId: draggable1.id, + draggableId: home1.id, impact, draggables, - droppable, + droppable: home, }); expect(result).toBe(null); @@ -438,21 +488,21 @@ describe('jump to next index', () => { const impact: DragImpact = { movement: { draggables: [], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + amount: patch(axis.line, home2.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { - droppableId: droppable.id, + droppableId: home.id, index: 1, }, direction: axis.direction, }; const result: ?Result = jumpToNextIndex({ isMovingForward: false, - draggableId: draggable2.id, + draggableId: home2.id, impact, draggables, - droppable, + droppable: home, }); if (!result) { @@ -461,9 +511,9 @@ describe('jump to next index', () => { it('should move the start of the draggable to the start of the previous draggable', () => { const expected: Position = moveToEdge({ - source: draggable2.page.withoutMargin, + source: home2.page.withoutMargin, sourceEdge: 'start', - destination: draggable1.page.withoutMargin, + destination: home1.page.withoutMargin, destinationEdge: 'start', destinationAxis: axis, }); @@ -474,12 +524,12 @@ describe('jump to next index', () => { it('should add the first draggable to the drag impact', () => { const expected: DragImpact = { movement: { - draggables: [draggable1.id], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + draggables: [home1.id], + amount: patch(axis.line, home2.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { - droppableId: droppable.id, + droppableId: home.id, // is now in the first position index: 0, }, @@ -494,21 +544,21 @@ describe('jump to next index', () => { const impact: DragImpact = { movement: { draggables: [], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), + amount: patch(axis.line, home3.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { - droppableId: droppable.id, + droppableId: home.id, index: 2, }, direction: axis.direction, }; const result: ?Result = jumpToNextIndex({ isMovingForward: false, - draggableId: draggable3.id, + draggableId: home3.id, impact, draggables, - droppable, + droppable: home, }); if (!result) { @@ -517,9 +567,9 @@ describe('jump to next index', () => { it('should move the start of the draggable to the start of the previous draggable', () => { const expected: Position = moveToEdge({ - source: draggable3.page.withoutMargin, + source: home3.page.withoutMargin, sourceEdge: 'start', - destination: draggable2.page.withoutMargin, + destination: home2.page.withoutMargin, destinationEdge: 'start', destinationAxis: axis, }); @@ -530,12 +580,12 @@ describe('jump to next index', () => { it('should add the second draggable to the drag impact', () => { const expected: DragImpact = { movement: { - draggables: [draggable2.id], - amount: patch(axis.line, draggable3.page.withMargin[axis.size]), + draggables: [home2.id], + amount: patch(axis.line, home3.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { - droppableId: droppable.id, + droppableId: home.id, // is now in the second position index: 1, }, @@ -553,22 +603,22 @@ describe('jump to next index', () => { // moving backwards towards the start again const impact: DragImpact = { movement: { - draggables: [draggable3.id], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + draggables: [home3.id], + amount: patch(axis.line, home2.page.withMargin[axis.size]), isBeyondStartPosition: true, }, direction: axis.direction, destination: { index: 2, - droppableId: droppable.id, + droppableId: home.id, }, }; const result: ?Result = jumpToNextIndex({ isMovingForward: false, - draggableId: draggable2.id, + draggableId: home2.id, impact, draggables, - droppable, + droppable: home, }); if (!result) { @@ -577,28 +627,28 @@ describe('jump to next index', () => { it('should move the end of the draggable to the end of the next draggable (which is its original position)', () => { const expected: Position = moveToEdge({ - source: draggable2.page.withoutMargin, + source: home2.page.withoutMargin, sourceEdge: 'end', // destination is itself as moving back to home - destination: draggable2.page.withoutMargin, + destination: home2.page.withoutMargin, destinationEdge: 'end', destinationAxis: axis, }); expect(result.pageCenter).toEqual(expected); // moved back to its original position - expect(result.pageCenter).toEqual(draggable2.page.withoutMargin.center); + expect(result.pageCenter).toEqual(home2.page.withoutMargin.center); }); it('should return an empty impact', () => { const expected: DragImpact = { movement: { draggables: [], - amount: patch(axis.line, draggable2.page.withMargin[axis.size]), + amount: patch(axis.line, home2.page.withMargin[axis.size]), isBeyondStartPosition: false, }, destination: { - droppableId: droppable.id, + droppableId: home.id, index: 1, }, direction: axis.direction, @@ -615,22 +665,22 @@ describe('jump to next index', () => { const impact: DragImpact = { movement: { // sorted by closest to where the draggable currently is - draggables: [draggable3.id, draggable2.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + draggables: [home3.id, home2.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: true, }, direction: axis.direction, destination: { index: 2, - droppableId: droppable.id, + droppableId: home.id, }, }; const result: ?Result = jumpToNextIndex({ isMovingForward: false, - draggableId: draggable1.id, + draggableId: home1.id, impact, draggables, - droppable, + droppable: home, }); if (!result) { @@ -639,9 +689,9 @@ describe('jump to next index', () => { it('should move the end of the draggable to the end of the previous draggable', () => { const expected: Position = moveToEdge({ - source: draggable1.page.withoutMargin, + source: home1.page.withoutMargin, sourceEdge: 'end', - destination: draggable2.page.withoutMargin, + destination: home2.page.withoutMargin, destinationEdge: 'end', destinationAxis: axis, }); @@ -653,12 +703,12 @@ describe('jump to next index', () => { const expected: DragImpact = { movement: { // draggable3 has been removed - draggables: [draggable2.id], - amount: patch(axis.line, draggable1.page.withMargin[axis.size]), + draggables: [home2.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: true, }, destination: { - droppableId: droppable.id, + droppableId: home.id, index: 1, }, direction: axis.direction, @@ -670,10 +720,258 @@ describe('jump to next index', () => { }); }); }); - }); - }); - describe('in foreign list', () => { + describe('in foreign list', () => { + it('should return null if there was no previous destination', () => { + + }); + + describe('moving backwards', () => { + it('should return null if attempting to move backwards beyond the start of the list', () => { + // moved home1 into the first position of the foreign list + const impact: DragImpact = { + movement: { + // Ordered by the closest impacted. + // Because we have moved into the first position it will be ordered 1-2-3 + draggables: [foreign1.id, foreign2.id, foreign3.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + // Always false when in another list + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + // it is now in the foreign droppable in the first position + droppableId: foreign.id, + index: 0, + }, + }; + + const result: ?Result = jumpToNextIndex({ + isMovingForward: false, + draggableId: home1.id, + impact, + droppable: foreign, + draggables, + }); + + expect(result).toBe(null); + }); + + describe('moving backwards into the first position of the list', () => { + // currently home1 is in the second position in front of foreign1 + const impact: DragImpact = { + movement: { + // Ordered by the closest impacted. + draggables: [foreign2.id, foreign3.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: foreign.id, + index: 1, + }, + }; + + console.log('executing'); + const result: ?Result = jumpToNextIndex({ + isMovingForward: false, + draggableId: home1.id, + impact, + droppable: foreign, + draggables, + }); + + if (!result) { + throw new Error('invalid test setup'); + } + + it('should move the start edge of home1 to the start edge of foreign1', () => { + const expected: Position = moveToEdge({ + source: home1.page.withoutMargin, + sourceEdge: 'start', + destination: foreign1.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); + + expect(result.pageCenter).toEqual(expected); + }); + + it('should add foreign1 to the impact', () => { + const expected: DragImpact = { + movement: { + draggables: [foreign1.id, foreign2.id, foreign3.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: foreign.id, + // now in the first position + index: 0, + }, + }; + expect(result.impact).toEqual(expected); + }); + }); + + describe('after moving forward in the list', () => { + + }); + }); + + describe('moving forwards', () => { + describe('moving forward one position', () => { + // moved home1 into the first position of the foreign list + const impact: DragImpact = { + movement: { + // Ordered by the closest impacted. + // Because we have moved into the first position it will be ordered 1-2-3 + draggables: [foreign1.id, foreign2.id, foreign3.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + // Always false when in another list + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + // it is now in the foreign droppable in the first position + droppableId: foreign.id, + index: 0, + }, + }; + + const result: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: home1.id, + impact, + droppable: foreign, + draggables, + }); + + if (!result) { + throw new Error('invalid test setup'); + } + + it('should move to the start edge of the dragging item to the end of foreign1', () => { + const expected = moveToEdge({ + source: home1.page.withoutMargin, + sourceEdge: 'start', + destination: foreign1.page.withMargin, + destinationEdge: 'end', + destinationAxis: foreign.axis, + }); + + expect(result.pageCenter).toEqual(expected); + }); + + it('should remove foreign1 when moving forward', () => { + const expected: DragImpact = { + movement: { + draggables: [foreign2.id, foreign3.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: foreign.id, + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); + }); + + describe('moving into last position of the list', () => { + // moved home1 into the second last position of the list + const impact: DragImpact = { + movement: { + // Ordered by the closest impacted. + draggables: [foreign3.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + // Always false when in another list + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + // it is now in the foreign droppable in the third position + droppableId: foreign.id, + index: 2, + }, + }; + + const result: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: home1.id, + impact, + droppable: foreign, + draggables, + }); + + if (!result) { + throw new Error('invalid test setup'); + } + + it('should move to the start edge of the dragging item to the end of foreign1', () => { + const expected = moveToEdge({ + source: home1.page.withoutMargin, + sourceEdge: 'start', + destination: foreign3.page.withMargin, + destinationEdge: 'end', + destinationAxis: foreign.axis, + }); + expect(result.pageCenter).toEqual(expected); + }); + + it('should remove foreign3 when moving forward', () => { + const expected: DragImpact = { + movement: { + draggables: [], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: foreign.id, + // bigger than the original list - in the forth position + index: 3, + }, + }; + + expect(result.impact).toEqual(expected); + }); + }); + + it('should return null if attempting to move beyond end of the list', () => { + // home1 is now in the last position of the list + const impact: DragImpact = { + movement: { + draggables: [], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: foreign.id, + // bigger than the original list - in the forth position + index: 3, + }, + }; + + const result: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: home1.id, + impact, + droppable: foreign, + draggables, + }); + + expect(result).toBe(null); + }); + }); + }); + }); }); }); From 5316f9ca6405c5aceb520e5c9232b2bb9e31dff8 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 16:48:20 +1000 Subject: [PATCH 062/117] jump to next index tests are looking good --- .../jump-to-next-index/in-foreign-list.js | 26 +- test/unit/state/jump-to-next-index.spec.js | 249 ++++++++++++------ 2 files changed, 188 insertions(+), 87 deletions(-) diff --git a/src/state/jump-to-next-index/in-foreign-list.js b/src/state/jump-to-next-index/in-foreign-list.js index 44ea3194fe..026deba1f4 100644 --- a/src/state/jump-to-next-index/in-foreign-list.js +++ b/src/state/jump-to-next-index/in-foreign-list.js @@ -35,8 +35,8 @@ export default ({ ); const currentIndex: number = location.index; - // Where the draggable will end up - const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; + const proposedIndex: number = isMovingForward ? currentIndex + 1 : currentIndex - 1; + const lastIndex: number = insideForeignDroppable.length - 1; // draggable is allowed to exceed the foreign droppables count by 1 if (proposedIndex > insideForeignDroppable.length) { @@ -48,16 +48,24 @@ export default ({ return null; } - const atProposedIndex: DraggableDimension = insideForeignDroppable[proposedIndex]; - // The draggable that we are going to move relative to + // Always moving relative to the draggable at the current index const movingRelativeTo: DraggableDimension = insideForeignDroppable[ - // We want to move relative to the previous draggable - // or to the first if there is no previous - Math.max(0, proposedIndex - 1) + // We want to move relative to the proposed index + // or if we are going beyond to the end of the list - use that index + Math.min(proposedIndex, lastIndex) ]; const sourceEdge: Edge = 'start'; - const destinationEdge: Edge = proposedIndex === 0 ? 'start' : 'end'; + const destinationEdge: Edge = (() => { + // moving past the last item + // in this case we are moving relative to the last item + // as there is nothing at the proposed index. + if (proposedIndex > lastIndex) { + return 'end'; + } + + return 'start'; + })(); const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, @@ -73,7 +81,7 @@ export default ({ // Stop displacing the closest draggable forward impact.movement.draggables.slice(1, impact.movement.draggables.length) : // Add the draggable that we are moving into the place of - [atProposedIndex.id, ...impact.movement.draggables]; + [movingRelativeTo.id, ...impact.movement.draggables]; const newImpact: DragImpact = { movement: { diff --git a/test/unit/state/jump-to-next-index.spec.js b/test/unit/state/jump-to-next-index.spec.js index cfb29ead8d..4e4b6dda3b 100644 --- a/test/unit/state/jump-to-next-index.spec.js +++ b/test/unit/state/jump-to-next-index.spec.js @@ -10,7 +10,6 @@ import type { Axis, DragMovement, DragImpact, - DroppableId, DraggableDimension, DroppableDimension, DraggableDimensionMap, @@ -19,6 +18,14 @@ import type { } from '../../../src/types'; describe('jump to next index', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + console.error.mockRestore(); + }); + [vertical, horizontal].forEach((axis: Axis) => { describe(`on the ${axis.direction} axis`, () => { const home: DroppableDimension = getDroppableDimension({ @@ -125,6 +132,41 @@ describe('jump to next index', () => { [foreign3.id]: foreign3, }; + it('should return null if there was no previous destination', () => { + const impact: DragImpact = { + movement: { + draggables: [], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + // no previous destination - should not happen when dragging with a keyboard + destination: null, + }; + + const result1: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: home1.id, + impact, + droppable: foreign, + draggables, + }); + + expect(result1).toEqual(null); + expect(console.error).toHaveBeenCalledTimes(1); + + const result2: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: home1.id, + impact, + droppable: foreign, + draggables, + }); + + expect(result2).toEqual(null); + expect(console.error).toHaveBeenCalledTimes(2); + }); + describe('in home list', () => { describe('jump forward', () => { it('should return null if cannot move forward', () => { @@ -722,12 +764,8 @@ describe('jump to next index', () => { }); describe('in foreign list', () => { - it('should return null if there was no previous destination', () => { - - }); - - describe('moving backwards', () => { - it('should return null if attempting to move backwards beyond the start of the list', () => { + describe('moving forwards', () => { + describe('moving forward one position', () => { // moved home1 into the first position of the foreign list const impact: DragImpact = { movement: { @@ -747,35 +785,67 @@ describe('jump to next index', () => { }; const result: ?Result = jumpToNextIndex({ - isMovingForward: false, + isMovingForward: true, draggableId: home1.id, impact, droppable: foreign, draggables, }); - expect(result).toBe(null); + if (!result) { + throw new Error('invalid test setup'); + } + + it('should move to the start edge of the dragging item to the start of foreign2', () => { + const expected = moveToEdge({ + source: home1.page.withoutMargin, + sourceEdge: 'start', + destination: foreign2.page.withMargin, + destinationEdge: 'start', + destinationAxis: foreign.axis, + }); + + expect(result.pageCenter).toEqual(expected); + }); + + it('should remove foreign1 when moving forward', () => { + const expected: DragImpact = { + movement: { + draggables: [foreign2.id, foreign3.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: foreign.id, + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - describe('moving backwards into the first position of the list', () => { - // currently home1 is in the second position in front of foreign1 + describe('moving into last position of the list', () => { + // moved home1 into the second last position of the list const impact: DragImpact = { movement: { // Ordered by the closest impacted. - draggables: [foreign2.id, foreign3.id], + draggables: [foreign3.id], amount: patch(axis.line, home1.page.withMargin[axis.size]), + // Always false when in another list isBeyondStartPosition: false, }, direction: axis.direction, destination: { + // it is now in the foreign droppable in the third position droppableId: foreign.id, - index: 1, + index: 2, }, }; - console.log('executing'); const result: ?Result = jumpToNextIndex({ - isMovingForward: false, + isMovingForward: true, draggableId: home1.id, impact, droppable: foreign, @@ -786,43 +856,67 @@ describe('jump to next index', () => { throw new Error('invalid test setup'); } - it('should move the start edge of home1 to the start edge of foreign1', () => { - const expected: Position = moveToEdge({ + it('should move to the start edge of the dragging item to the end of foreign1', () => { + const expected = moveToEdge({ source: home1.page.withoutMargin, sourceEdge: 'start', - destination: foreign1.page.withoutMargin, - destinationEdge: 'start', - destinationAxis: axis, + destination: foreign3.page.withMargin, + destinationEdge: 'end', + destinationAxis: foreign.axis, }); expect(result.pageCenter).toEqual(expected); }); - it('should add foreign1 to the impact', () => { + it('should remove foreign3 when moving forward', () => { const expected: DragImpact = { movement: { - draggables: [foreign1.id, foreign2.id, foreign3.id], + draggables: [], amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: false, }, direction: axis.direction, destination: { droppableId: foreign.id, - // now in the first position - index: 0, + // bigger than the original list - in the forth position + index: 3, }, }; + expect(result.impact).toEqual(expected); }); }); - describe('after moving forward in the list', () => { + it('should return null if attempting to move beyond end of the list', () => { + // home1 is now in the last position of the list + const impact: DragImpact = { + movement: { + draggables: [], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: foreign.id, + // bigger than the original list - in the forth position + index: 3, + }, + }; + const result: ?Result = jumpToNextIndex({ + isMovingForward: true, + draggableId: home1.id, + impact, + droppable: foreign, + draggables, + }); + + expect(result).toBe(null); }); }); - describe('moving forwards', () => { - describe('moving forward one position', () => { + describe('moving backwards', () => { + it('should return null if attempting to move backwards beyond the start of the list', () => { // moved home1 into the first position of the foreign list const impact: DragImpact = { movement: { @@ -842,7 +936,34 @@ describe('jump to next index', () => { }; const result: ?Result = jumpToNextIndex({ - isMovingForward: true, + isMovingForward: false, + draggableId: home1.id, + impact, + droppable: foreign, + draggables, + }); + + expect(result).toBe(null); + }); + + describe('moving backwards one position in list', () => { + // home1 is in the third position for foreign (one before the last) + const impact: DragImpact = { + movement: { + // Ordered by the closest impacted. + draggables: [foreign3.id], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: foreign.id, + index: 2, + }, + }; + + const result: ?Result = jumpToNextIndex({ + isMovingForward: false, draggableId: home1.id, impact, droppable: foreign, @@ -853,21 +974,22 @@ describe('jump to next index', () => { throw new Error('invalid test setup'); } - it('should move to the start edge of the dragging item to the end of foreign1', () => { - const expected = moveToEdge({ + it('should move to the start edge of foreign2', () => { + const expected: Position = moveToEdge({ source: home1.page.withoutMargin, sourceEdge: 'start', - destination: foreign1.page.withMargin, - destinationEdge: 'end', - destinationAxis: foreign.axis, + destination: foreign2.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, }); expect(result.pageCenter).toEqual(expected); }); - it('should remove foreign1 when moving forward', () => { + it('should add foreign2 to the drag impact', () => { const expected: DragImpact = { movement: { + // Ordered by the closest impacted. draggables: [foreign2.id, foreign3.id], amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: false, @@ -875,6 +997,7 @@ describe('jump to next index', () => { direction: axis.direction, destination: { droppableId: foreign.id, + // moved backwards index: 1, }, }; @@ -883,26 +1006,24 @@ describe('jump to next index', () => { }); }); - describe('moving into last position of the list', () => { - // moved home1 into the second last position of the list + describe('moving backwards into the first position of the list', () => { + // currently home1 is in the second position in front of foreign1 const impact: DragImpact = { movement: { // Ordered by the closest impacted. - draggables: [foreign3.id], + draggables: [foreign2.id, foreign3.id], amount: patch(axis.line, home1.page.withMargin[axis.size]), - // Always false when in another list isBeyondStartPosition: false, }, direction: axis.direction, destination: { - // it is now in the foreign droppable in the third position droppableId: foreign.id, - index: 2, + index: 1, }, }; const result: ?Result = jumpToNextIndex({ - isMovingForward: true, + isMovingForward: false, draggableId: home1.id, impact, droppable: foreign, @@ -913,63 +1034,35 @@ describe('jump to next index', () => { throw new Error('invalid test setup'); } - it('should move to the start edge of the dragging item to the end of foreign1', () => { - const expected = moveToEdge({ + it('should move the start edge of home1 to the start edge of foreign1', () => { + const expected: Position = moveToEdge({ source: home1.page.withoutMargin, sourceEdge: 'start', - destination: foreign3.page.withMargin, - destinationEdge: 'end', - destinationAxis: foreign.axis, + destination: foreign1.page.withoutMargin, + destinationEdge: 'start', + destinationAxis: axis, }); expect(result.pageCenter).toEqual(expected); }); - it('should remove foreign3 when moving forward', () => { + it('should add foreign1 to the impact', () => { const expected: DragImpact = { movement: { - draggables: [], + draggables: [foreign1.id, foreign2.id, foreign3.id], amount: patch(axis.line, home1.page.withMargin[axis.size]), isBeyondStartPosition: false, }, direction: axis.direction, destination: { droppableId: foreign.id, - // bigger than the original list - in the forth position - index: 3, + // now in the first position + index: 0, }, }; - expect(result.impact).toEqual(expected); }); }); - - it('should return null if attempting to move beyond end of the list', () => { - // home1 is now in the last position of the list - const impact: DragImpact = { - movement: { - draggables: [], - amount: patch(axis.line, home1.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: foreign.id, - // bigger than the original list - in the forth position - index: 3, - }, - }; - - const result: ?Result = jumpToNextIndex({ - isMovingForward: true, - draggableId: home1.id, - impact, - droppable: foreign, - draggables, - }); - - expect(result).toBe(null); - }); }); }); }); From b9c5609b0ca8e2007e01c4e3aae6daaf88d4c59d Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 20:18:16 +1000 Subject: [PATCH 063/117] fixing broken get-drag-impact tests --- src/state/get-drag-impact.js | 2 -- test/unit/state/get-drag-impact.spec.js | 12 +++++++----- test/unit/state/jump-to-next-index.spec.js | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 5117bfc6f3..0126398056 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -161,8 +161,6 @@ export default ({ isBeyondStartPosition: shouldDisplaceItemsForward, }; - console.log('mouse moved', ordered); - const impact: DragImpact = { movement, direction: axis.direction, diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index 2cc6dc4829..f5b6badee1 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -22,6 +22,8 @@ const droppableId: DroppableId = 'drop-1'; const origin: Position = { x: 0, y: 0 }; describe('get drag impact', () => { + // TODO: add tests for when not in home list + describe('vertical', () => { const droppable: DroppableDimension = getDroppableDimension({ id: droppableId, @@ -233,8 +235,8 @@ describe('get drag impact', () => { expect(impact.movement.amount).toEqual(expected); }); - it('should return the items that need to be moved', () => { - expect(impact.movement.draggables).toEqual([draggable2.id, draggable3.id]); + it('should return the items that need to be moved (sorted by the closest to the draggables current location)', () => { + expect(impact.movement.draggables).toEqual([draggable3.id, draggable2.id]); }); }); @@ -789,8 +791,8 @@ describe('get drag impact', () => { expect(impact.movement.amount).toEqual(expected); }); - it('should return the items that need to be moved', () => { - expect(impact.movement.draggables).toEqual([draggable2.id, draggable3.id]); + it('should return the items that need to be moved (sorted by closest impacted)', () => { + expect(impact.movement.draggables).toEqual([draggable3.id, draggable2.id]); }); }); @@ -1024,7 +1026,7 @@ describe('get drag impact', () => { expect(impact.movement.amount).toEqual(expected); }); - it('should return the items that need to be moved', () => { + it('should return the items that need to be moved (sorted by closest to the draggables current position)', () => { expect(impact.movement.draggables).toEqual([draggable1.id, draggable2.id]); }); }); diff --git a/test/unit/state/jump-to-next-index.spec.js b/test/unit/state/jump-to-next-index.spec.js index 4e4b6dda3b..f8f2d02743 100644 --- a/test/unit/state/jump-to-next-index.spec.js +++ b/test/unit/state/jump-to-next-index.spec.js @@ -168,7 +168,7 @@ describe('jump to next index', () => { }); describe('in home list', () => { - describe('jump forward', () => { + describe('moving forwards', () => { it('should return null if cannot move forward', () => { const impact: DragImpact = { movement: { @@ -498,7 +498,7 @@ describe('jump to next index', () => { }); }); - describe('jump backward', () => { + describe('moving backwards', () => { it('should return null if cannot move backward', () => { const impact: DragImpact = { movement: { From 48f7c5a080b6931a3a51a6c8b6bc1cd23be01887 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 7 Sep 2017 22:06:14 +1000 Subject: [PATCH 064/117] starting work on get-best-droppable tests --- src/state/dimension.js | 7 +- .../get-best-cross-axis-droppable.js | 12 +- .../move-to-new-droppable.old.js | 264 ------------------ .../get-best-cross-axis-droppable.spec.js | 140 ++++++++++ .../get-closest-draggable.spec.js | 19 ++ .../move-to-new-droppable.spec.js | 0 6 files changed, 172 insertions(+), 270 deletions(-) delete mode 100644 src/state/move-to-best-droppable/move-to-new-droppable.old.js create mode 100644 test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js create mode 100644 test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js create mode 100644 test/unit/state/move-to-best-droppable/move-to-new-droppable.spec.js diff --git a/src/state/dimension.js b/src/state/dimension.js index a5b4eb54a3..bf1495a331 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -117,7 +117,10 @@ type GetDroppableArgs = {| direction?: Direction, margin?: Margin, windowScroll?: Position, - scroll?: Position, + scroll ?: Position, + // Whether or not the droppable is currently enabled (can change at during a drag) + // defaults to true + isEnabled?: boolean, |} export const getDroppableDimension = ({ @@ -127,12 +130,14 @@ export const getDroppableDimension = ({ margin = noMargin, windowScroll = origin, scroll = origin, + isEnabled = true, }: GetDroppableArgs): DroppableDimension => { const withWindowScroll = getWithPosition(clientRect, windowScroll); const withWindowScrollAndMargin = getWithMargin(withWindowScroll, margin); const dimension: DroppableDimension = { id, + isEnabled, axis: direction === 'vertical' ? vertical : horizontal, scroll: { initial: scroll, diff --git a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js index 83e25dc82d..6aeb1dc0c3 100644 --- a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js +++ b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js @@ -42,9 +42,11 @@ export default ({ const axis: Axis = source.axis; const candidates: DroppableDimension[] = droppableMapToList(droppables) - // 1. Remove the source droppable from the list + // Remove the source droppable from the list .filter((droppable: DroppableDimension): boolean => droppable !== source) - // 2. Get only droppables that are on the desired side + // Remove any options that are not enabled + .filter((droppable: DroppableDimension): boolean => droppable.isEnabled) + // Get only droppables that are on the desired side .filter((droppable: DroppableDimension): boolean => { if (isMovingForward) { // is the droppable in front of the source on the cross axis? @@ -55,7 +57,7 @@ export default ({ return droppable.page.withMargin[axis.crossAxisEnd] <= source.page.withMargin[axis.crossAxisStart]; }) - // 3. is there any overlap on the main axis? + // Must have some overlap on the main axis .filter((droppable: DroppableDimension): boolean => { const sourceFragment: DimensionFragment = source.page.withMargin; const destinationFragment: DimensionFragment = droppable.page.withMargin; @@ -74,7 +76,7 @@ export default ({ isBetweenDestinationBounds(sourceFragment[axis.start]) || isBetweenDestinationBounds(sourceFragment[axis.end]); }) - // 4. Sort on the cross axis + // Sort on the cross axis .sort((a: DroppableDimension, b: DroppableDimension) => { const first: number = a.page.withMargin[axis.crossAxisStart]; const second: number = b.page.withMargin[axis.crossAxisStart]; @@ -84,7 +86,7 @@ export default ({ } return second - first; }) - // 5. Find the droppables that have the same cross axis value as the first item + // Find the droppables that have the same cross axis value as the first item .filter((droppable: DroppableDimension, index: number, array: DroppableDimension[]): boolean => droppable.page.withMargin[axis.crossAxisStart] === array[0].page.withMargin[axis.crossAxisStart] diff --git a/src/state/move-to-best-droppable/move-to-new-droppable.old.js b/src/state/move-to-best-droppable/move-to-new-droppable.old.js deleted file mode 100644 index 88e08e647d..0000000000 --- a/src/state/move-to-best-droppable/move-to-new-droppable.old.js +++ /dev/null @@ -1,264 +0,0 @@ -// @flow -import { subtract, patch } from '../position'; -import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import moveToEdge from '../move-to-edge'; -import noImpact from '../no-impact'; -import type { - Axis, - Position, - DragImpact, - DraggableId, - DraggableDimension, - DroppableDimension, - DraggableDimensionMap, - DraggableLocation, -} from '../../types'; - -type Args = {| - // the current center position of the draggable - center: Position, - // the draggable that is dragging and needs to move - draggable: DraggableDimension, - // what the draggable is moving towards - // can be null if the destination is empty - target: ?DraggableDimension, - // the droppable the draggable is currently in - source: DroppableDimension, - // the droppable the draggable is moving to - destination: DroppableDimension, - // the source location of the draggable - home: DraggableLocation, - // the current drag impact - impact: DragImpact, - // all the draggables in the system - draggables: DraggableDimensionMap, -|} - -export type Result = {| - // how far the draggable needs to move to be in its new home - center: Position, - // The impact of the movement - impact: DragImpact, -|} - -export default ({ - center, - source, - destination, - draggable, - target, - home, - draggables, -}: Args): ?Result => { - const destinationAxis: Axis = destination.axis; - const sourceAxis: Axis = source.axis; - const amount: Position = patch( - destinationAxis.line, - draggable.page.withMargin[destinationAxis.size] - ); - // 1. Moving to an empty droppable - - if (!target) { - // Move to start edge of the destination - // based on the axis of the destination - - // start edge of draggable needs to line up - // with start edge of destination - const newCenter: Position = moveToEdge({ - source: draggable.page.withMargin, - sourceEdge: 'start', - destination: destination.page.withMargin, - destinationEdge: 'start', - destinationAxis, - }); - - const newImpact: DragImpact = { - movement: { - draggables: [], - amount, - // TODO: not sure what this should be - isBeyondStartPosition: false, - }, - direction: destinationAxis.direction, - destination: { - droppableId: destination.id, - index: 0, - }, - }; - - return { - center: newCenter, - impact: newImpact, - }; - } - - // 2. Moving to a populated droppable - - const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable( - destination, draggables - ); - - const isGoingBeforeTarget: boolean = center[sourceAxis.line] < - target.page.withMargin.center[sourceAxis.line]; - - const targetIndex: number = insideDestination.indexOf(target); - const proposedIndex: number = isGoingBeforeTarget ? targetIndex : targetIndex + 1; - - if (targetIndex === -1) { - console.error('could not find target inside destination'); - return null; - } - - const isReturningToHomeList = destination.id === draggable.droppableId; - - if (isReturningToHomeList) { - const proposedIndex: number = targetIndex; - - console.group('returning to home list'); - console.log('target index', targetIndex); - console.log('proposed index', proposedIndex); - // returning to original position - if (targetIndex === home.index) { - console.log('returning to original position'); - const newCenter: Position = draggable.page.withoutMargin.center; - const newImpact: DragImpact = { - movement: { - draggables: [], - amount, - // TODO: not sure what this should be - isBeyondStartPosition: false, - }, - direction: destinationAxis.direction, - destination: { - droppableId: destination.id, - index: home.index, - }, - }; - console.groupEnd(); - - return { - center: newCenter, - impact: newImpact, - }; - } - - console.info('returning to original list - but not in original position'); - console.log('is going before target', isGoingBeforeTarget); - - // need to put into the correct position and have the correct impact - - const isMovingBeyondHome = targetIndex > home.index; - console.log('is moving beyond home', isMovingBeyondHome); - - const isMovingRelativeToSelf = target.id === draggable.id; - console.log('target id', target.id); - - console.log('is moving relative to self', isMovingRelativeToSelf); - - const sourceEdge = (() => { - if (isMovingBeyondHome) { - return isGoingBeforeTarget ? 'end' : 'end'; - } - return 'start'; - })(); - - const destinationEdge = (() => { - if (isMovingBeyondHome) { - return isGoingBeforeTarget ? 'end' : 'end'; - } - return 'start'; - })(); - - console.log('source edge', sourceEdge); - console.log('destination edge', destinationEdge); - - const newCenter: Position = moveToEdge({ - source: draggable.page.withoutMargin, - sourceEdge, - destination: target.page.withMargin, - destinationEdge, - destinationAxis, - }); - - const needsToMove: DraggableId[] = (() => { - if (isMovingBeyondHome) { - console.group('movingBeyondHome'); - console.log('original', insideDestination); - const result = [...insideDestination]; - // result.splice(home.index, 1); - console.log('stripped', result); - return result.slice(home.index + 1, proposedIndex + 1); - console.groupEnd(); - } - return insideDestination.slice(proposedIndex, home.index); - })().map(d => d.id); - - console.log('moved', needsToMove); - - const newImpact: DragImpact = { - movement: { - draggables: needsToMove, - amount, - // TODO: not sure what this should be - isBeyondStartPosition: isMovingBeyondHome, - }, - direction: destinationAxis.direction, - destination: { - droppableId: destination.id, - index: proposedIndex, - }, - }; - - console.log('impact', newImpact); - console.groupEnd(); - - return { - center: newCenter, - impact: newImpact, - }; - } - - // 1. If isGoingBefore: need to move draggable start edge to start edge of target - // Then need to move the target and everything after it forward - // 2. If is going after: need to move draggable start edge to the end of the target - // Then need to move everything after the target forward - // const isGoingBeforeTarget: boolean = center[sourceAxis.line] < - // target.page.withMargin.center[sourceAxis.line]; - - // const proposedIndex: number = isGoingBeforeTarget ? targetIndex : targetIndex + 1; - - const newCenter: Position = moveToEdge({ - source: draggable.page.withoutMargin, - sourceEdge: 'start', - destination: target.page.withMargin, - destinationEdge: isGoingBeforeTarget ? 'start' : 'end', - destinationAxis, - }); - - // need to get the index of the draggable that we are moving relative to - - const needsToMove: DraggableId[] = insideDestination - .slice(proposedIndex, insideDestination.length) - .map((dimension: DraggableDimension): DraggableId => dimension.id); - - console.log('cross axis movement', needsToMove); - - const newImpact: DragImpact = { - movement: { - draggables: needsToMove, - amount, - // TODO: not sure what this should be - isBeyondStartPosition: false, - }, - direction: destinationAxis.direction, - destination: { - droppableId: destination.id, - index: proposedIndex, - }, - }; - - return { - center: newCenter, - impact: newImpact, - }; -}; diff --git a/test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js b/test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js new file mode 100644 index 0000000000..4fbb8afac2 --- /dev/null +++ b/test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js @@ -0,0 +1,140 @@ +// @flow +import getBestCrossAxisDroppable from '../../../../src/state/move-to-best-droppable/get-best-cross-axis-droppable'; +import { getDroppableDimension } from '../../../../src/state/dimension'; +import getClientRect from '../../../utils/get-client-rect'; +import { horizontal, vertical } from '../../../../src/state/axis'; +import type { + Axis, + DroppableDimension, + DroppableDimensionMap, +} from '../../../../src/types'; + +describe('get best cross axis droppable', () => { + // TODO: horizontal + [vertical].forEach((axis: Axis) => { + it('should return the first droppable on the cross axis', () => { + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 20, + right: 30, + bottom: 10, + }), + }); + const forward = getDroppableDimension({ + id: 'forward', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 30, + right: 40, + bottom: 10, + }), + }); + const droppables: DroppableDimensionMap = { + [source.id]: source, + [forward.id]: forward, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + + expect(result).toBe(forward); + }); + + it('should exclude options that are not in the desired direction', () => { + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 20, + right: 30, + bottom: 10, + }), + }); + const behind = getDroppableDimension({ + id: 'behind', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 0, + right: 10, + bottom: 10, + }), + }); + const droppables: DroppableDimensionMap = { + [behind.id]: behind, + [source.id]: source, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + + expect(result).toBe(null); + }); + + it('should exclude options that are not enabled', () => { + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 20, + right: 30, + bottom: 10, + }), + }); + const forward = getDroppableDimension({ + id: 'forward', + isEnabled: false, + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 30, + right: 40, + bottom: 10, + }), + }); + const droppables: DroppableDimensionMap = { + [source.id]: source, + [forward.id]: forward, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + + expect(result).toBe(null); + }); + + it('should exclude options that do not overlap on the main axis', () => { + + }); + + describe('more than one option share the same crossAxisStart value', () => { + it('should return a droppable where the center position (axis.line) of the draggable draggable sits within the size of a droppable', () => { + + }); + + describe('center point is not contained within a droppable', () => { + it('should return the droppable that has the closest corner', () => { + + }); + }); + }); + }); +}); diff --git a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js new file mode 100644 index 0000000000..6a1c3d8784 --- /dev/null +++ b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js @@ -0,0 +1,19 @@ +// @flow +import { horizontal, vertical } from '../../../../src/state/axis'; +import type { Axis } from '../../../../src/types'; + +describe('get closest draggable', () => { + [horizontal, vertical].forEach((axis: Axis) => { + it('should remove draggables that are not within the current visible bounds of a droppable', () => { + + }); + + it('should return the closest draggable', () => { + + }); + + it('should return the draggable that is first on the main axis in the event of tie', () => { + + }); + }); +}); diff --git a/test/unit/state/move-to-best-droppable/move-to-new-droppable.spec.js b/test/unit/state/move-to-best-droppable/move-to-new-droppable.spec.js new file mode 100644 index 0000000000..e69de29bb2 From 8af5a4e14f0c5e9831f84b4ab89671aff95aacee Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 08:15:35 +1000 Subject: [PATCH 065/117] renmaing jump to next to move to next --- .../in-foreign-list.js | 4 +- .../in-home-list.js | 4 +- .../index.js | 7 +- .../move-to-next-index-types.js} | 0 src/state/reducer.js | 6 +- ...dex.spec.js => move-to-next-index.spec.js} | 82 +++++++++++++------ 6 files changed, 72 insertions(+), 31 deletions(-) rename src/state/{jump-to-next-index => move-to-next-index}/in-foreign-list.js (95%) rename src/state/{jump-to-next-index => move-to-next-index}/in-home-list.js (96%) rename src/state/{jump-to-next-index => move-to-next-index}/index.js (70%) rename src/state/{jump-to-next-index/jump-to-next-index-types.js => move-to-next-index/move-to-next-index-types.js} (100%) rename test/unit/state/{jump-to-next-index.spec.js => move-to-next-index.spec.js} (94%) diff --git a/src/state/jump-to-next-index/in-foreign-list.js b/src/state/move-to-next-index/in-foreign-list.js similarity index 95% rename from src/state/jump-to-next-index/in-foreign-list.js rename to src/state/move-to-next-index/in-foreign-list.js index 026deba1f4..b46686e93c 100644 --- a/src/state/jump-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -3,7 +3,7 @@ import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import { patch } from '../position'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; -import type { Args, Result } from './jump-to-next-index-types'; +import type { Args, Result } from './move-to-next-index-types'; import type { DraggableLocation, DraggableDimension, @@ -21,7 +21,7 @@ export default ({ draggables, }: Args): ?Result => { if (!impact.destination) { - console.error('cannot jump to next index when there is not previous destination'); + console.error('cannot move to next index when there is not previous destination'); return null; } diff --git a/src/state/jump-to-next-index/in-home-list.js b/src/state/move-to-next-index/in-home-list.js similarity index 96% rename from src/state/jump-to-next-index/in-home-list.js rename to src/state/move-to-next-index/in-home-list.js index 9f2fbe5bc3..4a1a376cb1 100644 --- a/src/state/jump-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -4,7 +4,7 @@ import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import { patch } from '../position'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; -import type { Args, Result } from './jump-to-next-index-types'; +import type { Args, Result } from './move-to-next-index-types'; import type { DraggableLocation, DraggableDimension, @@ -28,7 +28,7 @@ export default ({ draggables, }: Args): ?Result => { if (!impact.destination) { - console.error('cannot jump to next index when there is not previous destination'); + console.error('cannot move to next index when there is not previous destination'); return null; } diff --git a/src/state/jump-to-next-index/index.js b/src/state/move-to-next-index/index.js similarity index 70% rename from src/state/jump-to-next-index/index.js rename to src/state/move-to-next-index/index.js index 4f6b692e56..345045d473 100644 --- a/src/state/jump-to-next-index/index.js +++ b/src/state/move-to-next-index/index.js @@ -1,7 +1,7 @@ // @flow import inHomeList from './in-home-list'; import inForeignList from './in-foreign-list'; -import type { Args, Result } from './jump-to-next-index-types'; +import type { Args, Result } from './move-to-next-index-types'; import type { DraggableDimension } from '../../types'; export default (args: Args): ?Result => { @@ -10,6 +10,11 @@ export default (args: Args): ?Result => { const draggable: DraggableDimension = draggables[draggableId]; const isInHomeList: boolean = draggable.droppableId === droppable.id; + // Cannot move in list if the list is not enabled (can still cross axis move) + if (!droppable.isEnabled) { + return null; + } + if (isInHomeList) { return inHomeList(args); } diff --git a/src/state/jump-to-next-index/jump-to-next-index-types.js b/src/state/move-to-next-index/move-to-next-index-types.js similarity index 100% rename from src/state/jump-to-next-index/jump-to-next-index-types.js rename to src/state/move-to-next-index/move-to-next-index-types.js diff --git a/src/state/reducer.js b/src/state/reducer.js index d5809fd4b9..24768c9ecc 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -22,8 +22,8 @@ import type { TypeId, } from '../types'; import { add, subtract, negate } from './position'; import getDragImpact from './get-drag-impact'; -import jumpToNextIndex from './jump-to-next-index/'; -import type { Result as JumpToNextResult } from './jump-to-next-index/jump-to-next-index-types'; +import moveToNextIndex from './move-to-next-index/'; +import type { Result as MoveToNextResult } from './move-to-next-index/move-to-next-index-types'; import type { Result as MoveToNewDroppable } from './move-to-best-droppable/move-to-new-droppable'; import moveToBestDroppable from './move-to-best-droppable/'; @@ -398,7 +398,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { const existing: DragState = state.drag; const isMovingForward: boolean = action.type === 'MOVE_FORWARD'; - const result: ?JumpToNextResult = jumpToNextIndex({ + const result: ?MoveToNextResult = moveToNextIndex({ isMovingForward, draggableId: existing.current.id, impact: existing.impact, diff --git a/test/unit/state/jump-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js similarity index 94% rename from test/unit/state/jump-to-next-index.spec.js rename to test/unit/state/move-to-next-index.spec.js index f8f2d02743..d94a27079e 100644 --- a/test/unit/state/jump-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -1,6 +1,6 @@ // @flow -import jumpToNextIndex from '../../../src/state/jump-to-next-index/'; -import type { Result } from '../../../src/state/jump-to-next-index/jump-to-next-index-types'; +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 { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; import getClientRect from '../../utils/get-client-rect'; import moveToEdge from '../../../src/state/move-to-edge'; @@ -17,7 +17,7 @@ import type { Position, } from '../../../src/types'; -describe('jump to next index', () => { +describe('move to next index', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); }); @@ -132,6 +132,42 @@ describe('jump to next index', () => { [foreign3.id]: foreign3, }; + it('should return null if the droppable is disabled', () => { + const disabled: DroppableDimension = getDroppableDimension({ + id: 'disabled', + isEnabled: false, + direction: axis.direction, + clientRect: getClientRect({ + top: 2001, + left: 2001, + bottom: 3000, + right: 3000, + }), + }); + const impact: DragImpact = { + movement: { + draggables: [], + amount: patch(axis.line, home1.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: disabled.id, + index: 0, + }, + }; + + const result: ?Result = moveToNextIndex({ + isMovingForward: true, + draggableId: home1.id, + impact, + droppable: disabled, + draggables, + }); + + expect(result).toEqual(null); + }); + it('should return null if there was no previous destination', () => { const impact: DragImpact = { movement: { @@ -144,7 +180,7 @@ describe('jump to next index', () => { destination: null, }; - const result1: ?Result = jumpToNextIndex({ + const result1: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home1.id, impact, @@ -155,7 +191,7 @@ describe('jump to next index', () => { expect(result1).toEqual(null); expect(console.error).toHaveBeenCalledTimes(1); - const result2: ?Result = jumpToNextIndex({ + const result2: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home1.id, impact, @@ -183,7 +219,7 @@ describe('jump to next index', () => { }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home3.id, impact, @@ -211,7 +247,7 @@ describe('jump to next index', () => { direction: axis.direction, destination, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home1.id, impact, @@ -269,7 +305,7 @@ describe('jump to next index', () => { direction: axis.direction, destination, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home2.id, impact, @@ -327,7 +363,7 @@ describe('jump to next index', () => { index: 1, }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home1.id, impact, @@ -390,7 +426,7 @@ describe('jump to next index', () => { droppableId: home.id, }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home2.id, impact, @@ -399,7 +435,7 @@ describe('jump to next index', () => { }); if (!result) { - throw new Error('invalid result of jumpToNextIndex'); + throw new Error('invalid result of moveToNextIndex'); } it('should move the start of the dragging item to the end of the previous item (which its original position)', () => { @@ -452,7 +488,7 @@ describe('jump to next index', () => { }, }; // moving draggable3 forward one position - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home3.id, impact, @@ -513,7 +549,7 @@ describe('jump to next index', () => { }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: false, draggableId: home1.id, impact, @@ -539,7 +575,7 @@ describe('jump to next index', () => { }, direction: axis.direction, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: false, draggableId: home2.id, impact, @@ -595,7 +631,7 @@ describe('jump to next index', () => { }, direction: axis.direction, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: false, draggableId: home3.id, impact, @@ -655,7 +691,7 @@ describe('jump to next index', () => { droppableId: home.id, }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: false, draggableId: home2.id, impact, @@ -717,7 +753,7 @@ describe('jump to next index', () => { droppableId: home.id, }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: false, draggableId: home1.id, impact, @@ -784,7 +820,7 @@ describe('jump to next index', () => { }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home1.id, impact, @@ -844,7 +880,7 @@ describe('jump to next index', () => { }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home1.id, impact, @@ -903,7 +939,7 @@ describe('jump to next index', () => { }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: true, draggableId: home1.id, impact, @@ -935,7 +971,7 @@ describe('jump to next index', () => { }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: false, draggableId: home1.id, impact, @@ -962,7 +998,7 @@ describe('jump to next index', () => { }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: false, draggableId: home1.id, impact, @@ -1022,7 +1058,7 @@ describe('jump to next index', () => { }, }; - const result: ?Result = jumpToNextIndex({ + const result: ?Result = moveToNextIndex({ isMovingForward: false, draggableId: home1.id, impact, From c97a48c20796a2996c1097fea6817e6b0c3cf9f0 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 08:25:01 +1000 Subject: [PATCH 066/117] removing temp type addition --- src/state/reducer.js | 7 +++++-- src/types.js | 3 --- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/state/reducer.js b/src/state/reducer.js index 24768c9ecc..d9f3e1a5b5 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -398,6 +398,11 @@ export default (state: State = clean('IDLE'), action: Action): State => { const existing: DragState = state.drag; const isMovingForward: boolean = action.type === 'MOVE_FORWARD'; + if (!existing.impact.destination) { + console.error('cannot move if there is no previous destination'); + return clean(); + } + const result: ?MoveToNextResult = moveToNextIndex({ isMovingForward, draggableId: existing.current.id, @@ -440,7 +445,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { } if (action.type === 'CROSS_AXIS_MOVE_FORWARD' || action.type === 'CROSS_AXIS_MOVE_BACKWARD') { - console.log('trying to moving on cross axis', action.type); if (state.phase !== 'DRAGGING') { console.error('cannot move cross axis when not dragging'); return clean(); @@ -457,7 +461,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { } const current: CurrentDrag = state.drag.current; - const draggableId: DraggableId = current.id; const center: Position = current.page.center; const droppableId: DroppableId = state.drag.impact.destination.droppableId; diff --git a/src/types.js b/src/types.js index 2ac8f1c8d5..eaf0e3fafa 100644 --- a/src/types.js +++ b/src/types.js @@ -155,9 +155,6 @@ export type CurrentDrag = {| withinDroppable: WithinDroppable, // whether or not movements should be animated shouldAnimate: boolean, - droppable: DraggableLocation & {| - startIndex: number, - |} |} // published when a drag starts From dae315ef345ec8ab5a2a053abe273666f1f49215 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 11:45:16 +1000 Subject: [PATCH 067/117] adding tests for get-best-cross-axis-droppable --- src/state/get-drag-impact.js | 3 +- .../get-best-cross-axis-droppable.js | 10 +- .../get-best-cross-axis-droppable.spec.js | 497 +++++++++++++++++- 3 files changed, 497 insertions(+), 13 deletions(-) diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 0126398056..529bedc608 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -69,14 +69,13 @@ export default ({ const newCenter = withinDroppable.center; const draggingDimension: DraggableDimension = draggables[draggableId]; const droppable: DroppableDimension = droppables[droppableId]; + const axis: Axis = droppable.axis; const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( droppable, draggables, ); - const axis: Axis = droppable.axis; - // not considering margin so that items move based on visible edges const draggableCenter: Position = draggingDimension.page.withoutMargin.center; const isBeyondStartPosition: boolean = newCenter[axis.line] - draggableCenter[axis.line] > 0; diff --git a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js index 6aeb1dc0c3..02fcb8a82d 100644 --- a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js +++ b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js @@ -105,8 +105,7 @@ export default ({ // At this point we have a number of candidates that // all have the same axis.crossAxisStart value. - // 1. Check to see if the center position is within the size of a Droppable on the main axis - + // Check to see if the center position is within the size of a Droppable on the main axis const contains: DroppableDimension[] = candidates .filter((droppable: DroppableDimension) => { const isWithinDroppable = isWithin( @@ -120,7 +119,7 @@ export default ({ return contains[0]; } - // the center point of the draggable falls on the boundary between to droppables + // The center point of the draggable falls on the boundary between two droppables if (contains.length > 1) { // sort on the main axis and choose the first return contains.sort((a: DroppableDimension, b: DroppableDimension) => ( @@ -128,8 +127,7 @@ export default ({ ))[0]; } - // 2. The center is not contained within any droppable - + // The center is not contained within any droppable // 1. Find the candidate that has the closest corner // 2. If there is a tie - choose the one that is first on the main axis return candidates.sort((a: DroppableDimension, b: DroppableDimension) => { @@ -141,7 +139,7 @@ export default ({ return first - second; } - // they both have the same distance - + // They both have the same distance - // choose the one that is first on the main axis return a.page.withMargin[axis.start] - b.page.withMargin[axis.start]; })[0]; diff --git a/test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js b/test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js index 4fbb8afac2..2e3358aa5a 100644 --- a/test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js +++ b/test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js @@ -2,17 +2,20 @@ import getBestCrossAxisDroppable from '../../../../src/state/move-to-best-droppable/get-best-cross-axis-droppable'; import { getDroppableDimension } from '../../../../src/state/dimension'; import getClientRect from '../../../utils/get-client-rect'; +import { add } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; import type { Axis, + Position, DroppableDimension, DroppableDimensionMap, } from '../../../../src/types'; describe('get best cross axis droppable', () => { - // TODO: horizontal - [vertical].forEach((axis: Axis) => { - it('should return the first droppable on the cross axis', () => { + describe('on the vertical axis', () => { + const axis: Axis = vertical; + + it('should return the first droppable on the cross axis when moving forward', () => { const source = getDroppableDimension({ id: 'source', direction: axis.direction, @@ -48,6 +51,43 @@ describe('get best cross axis droppable', () => { expect(result).toBe(forward); }); + it('should return the first droppable on the cross axis when moving backward', () => { + const behind = getDroppableDimension({ + id: 'behind', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 20, + right: 30, + bottom: 10, + }), + }); + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 30, + right: 40, + bottom: 10, + }), + }); + const droppables: DroppableDimensionMap = { + [behind.id]: behind, + [source.id]: source, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + // moving backwards + isMovingForward: false, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + + expect(result).toBe(behind); + }); + it('should exclude options that are not in the desired direction', () => { const source = getDroppableDimension({ id: 'source', @@ -80,8 +120,16 @@ describe('get best cross axis droppable', () => { source, droppables, }); - expect(result).toBe(null); + + // checking that it would have been returned if was moving in the other direction + const result2: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: false, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + expect(result2).toBe(behind); }); it('should exclude options that are not enabled', () => { @@ -95,8 +143,313 @@ describe('get best cross axis droppable', () => { bottom: 10, }), }); + const disabled = getDroppableDimension({ + id: 'disabled', + isEnabled: false, + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 30, + right: 40, + bottom: 10, + }), + }); + const droppables: DroppableDimensionMap = { + [source.id]: source, + [disabled.id]: disabled, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + + expect(result).toBe(null); + }); + + it('should exclude options that do not overlap on the main axis', () => { + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 20, + right: 30, + bottom: 10, + }), + }); + const noOverlap = getDroppableDimension({ + id: 'noOverlap', + direction: axis.direction, + clientRect: getClientRect({ + // top is below where the source ended + top: 11, + left: 30, + right: 40, + bottom: 20, + }), + }); + + const droppables: DroppableDimensionMap = { + [source.id]: source, + [noOverlap.id]: noOverlap, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + + expect(result).toBe(null); + }); + + describe('more than one option share the same crossAxisStart value', () => { + // this happens when two lists sit on top of one another + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 0, + right: 20, + bottom: 100, + }), + }); + const sibling1 = getDroppableDimension({ + id: 'sibling1', + direction: axis.direction, + clientRect: getClientRect({ + // not the same top value as source + top: 20, + // shares the left edge with the source + left: 20, + right: 40, + bottom: 40, + }), + }); + const sibling2 = getDroppableDimension({ + id: 'sibling2', + direction: axis.direction, + clientRect: getClientRect({ + // shares the bottom edge with sibling1 + top: 40, + // shares the left edge with the source + left: 20, + right: 40, + bottom: 60, + }), + }); + const droppables: DroppableDimensionMap = { + [source.id]: source, + [sibling1.id]: sibling1, + [sibling2.id]: sibling2, + }; + + it('should return a droppable where the center position (axis.line) of the draggable draggable sits within the size of a droppable', () => { + // sitting inside source - but within the size of sibling2 on the main axis + const center: Position = { + y: 50, + x: 10, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: center, + source, + droppables, + }); + + expect(result).toBe(sibling2); + }); + + describe('center point is not contained within a droppable', () => { + it('should return the droppable that has the closest corner', () => { + // Choosing a point that is above the first sibling + const center: Position = { + // above sibling 1 + y: 10, + x: 10, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: center, + source, + droppables, + }); + + expect(result).toBe(sibling1); + }); + + it('should choose the droppable that is furthest back (closest to {x: 0, y: 0} on the screen) in the event of a tie', () => { + // Choosing a point that is above the first sibling + const center: Position = { + // this line is shared between sibling1 and sibling2 + y: 40, + x: 10, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: center, + source, + droppables, + }); + + expect(result).toBe(sibling1); + + // checking that center position was selected correctly + const center2: Position = add(center, { x: 0, y: 1 }); + const result2: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: center2, + source, + droppables, + }); + expect(result2).toBe(sibling2); + }); + }); + }); + }); + + describe('on the horizontal axis', () => { + const axis: Axis = horizontal; + + it('should return the first droppable on the cross axis when moving forward', () => { + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 0, + right: 20, + bottom: 20, + }), + }); const forward = getDroppableDimension({ id: 'forward', + direction: axis.direction, + clientRect: getClientRect({ + top: 20, + left: 0, + right: 20, + bottom: 30, + }), + }); + const droppables: DroppableDimensionMap = { + [source.id]: source, + [forward.id]: forward, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + + expect(result).toBe(forward); + }); + + it('should return the first droppable on the cross axis when moving backward', () => { + const behind = getDroppableDimension({ + id: 'behind', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 0, + right: 20, + bottom: 10, + }), + }); + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 10, + left: 0, + right: 20, + bottom: 20, + }), + }); + const droppables: DroppableDimensionMap = { + [behind.id]: behind, + [source.id]: source, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + // moving backwards + isMovingForward: false, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + + expect(result).toBe(behind); + }); + + it('should exclude options that are not in the desired direction', () => { + const behind = getDroppableDimension({ + id: 'behind', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 0, + right: 20, + bottom: 10, + }), + }); + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 10, + left: 0, + right: 20, + bottom: 20, + }), + }); + const droppables: DroppableDimensionMap = { + [behind.id]: behind, + [source.id]: source, + }; + + // now moving in the other direction + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + expect(result).toBe(null); + + // Ensuring that normally it would be returned if moving in the right direction + const result2: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: false, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + expect(result2).toBe(behind); + }); + + it('should exclude options that are not enabled', () => { + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 20, + right: 30, + bottom: 10, + }), + }); + const disabled = getDroppableDimension({ + id: 'disabled', isEnabled: false, direction: axis.direction, clientRect: getClientRect({ @@ -108,7 +461,7 @@ describe('get best cross axis droppable', () => { }); const droppables: DroppableDimensionMap = { [source.id]: source, - [forward.id]: forward, + [disabled.id]: disabled, }; const result: ?DroppableDimension = getBestCrossAxisDroppable({ @@ -122,17 +475,151 @@ describe('get best cross axis droppable', () => { }); it('should exclude options that do not overlap on the main axis', () => { + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 0, + right: 20, + bottom: 10, + }), + }); + const noOverlap = getDroppableDimension({ + id: 'noOverlap', + direction: axis.direction, + clientRect: getClientRect({ + // comes after the source + top: 10, + // but its left value is > the rigt of the source + left: 30, + right: 40, + bottom: 20, + }), + }); + + const droppables: DroppableDimensionMap = { + [source.id]: source, + [noOverlap.id]: noOverlap, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: source.page.withMargin.center, + source, + droppables, + }); + expect(result).toBe(null); }); describe('more than one option share the same crossAxisStart value', () => { + // this happens when two lists sit side by side + const source = getDroppableDimension({ + id: 'source', + direction: axis.direction, + clientRect: getClientRect({ + top: 0, + left: 0, + right: 100, + bottom: 10, + }), + }); + const sibling1 = getDroppableDimension({ + id: 'sibling1', + direction: axis.direction, + clientRect: getClientRect({ + // shares an edge with the source + top: 10, + // shares the left edge with the source + left: 20, + right: 40, + bottom: 20, + }), + }); + const sibling2 = getDroppableDimension({ + id: 'sibling2', + direction: axis.direction, + clientRect: getClientRect({ + // shares an edge with the source + top: 10, + // shares the left edge with the source + left: 40, + right: 60, + bottom: 20, + }), + }); + const droppables: DroppableDimensionMap = { + [source.id]: source, + [sibling1.id]: sibling1, + [sibling2.id]: sibling2, + }; + it('should return a droppable where the center position (axis.line) of the draggable draggable sits within the size of a droppable', () => { + // sitting inside source - but within the size of sibling2 on the main axis + const center: Position = { + y: 5, + x: 50, + }; + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: center, + source, + droppables, + }); + + expect(result).toBe(sibling2); }); describe('center point is not contained within a droppable', () => { it('should return the droppable that has the closest corner', () => { + // Choosing a point that is before the first sibling + const center: Position = { + // above sibling 1 + y: 5, + // before the left value of sibling 1 + x: 10, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: center, + source, + droppables, + }); + + expect(result).toBe(sibling1); + }); + + it('should choose the droppable that is furthest back (closest to {x: 0, y: 0} on the screen) in the event of a tie', () => { + // Choosing a point that is above the first sibling + const center: Position = { + y: 5, + // this line is shared between sibling1 and sibling2 + x: 40, + }; + + const result: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: center, + source, + droppables, + }); + + expect(result).toBe(sibling1); + + // checking that center point is correct + + const center2: Position = add(center, { y: 0, x: 1 }); + const result2: ?DroppableDimension = getBestCrossAxisDroppable({ + isMovingForward: true, + pageCenter: center2, + source, + droppables, + }); + expect(result2).toBe(sibling2); }); }); }); From 4bd60f74bd1539b2072b90a574fd3695502b46f4 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 14:02:06 +1000 Subject: [PATCH 068/117] adding tests for get-closest-draggable --- src/state/is-within.js | 6 + .../get-best-cross-axis-droppable.js | 4 +- .../get-closest-draggable.js | 31 ++- test/unit/state/is-within.spec.js | 28 +++ .../get-closest-draggable.spec.js | 233 +++++++++++++++++- 5 files changed, 282 insertions(+), 20 deletions(-) create mode 100644 src/state/is-within.js create mode 100644 test/unit/state/is-within.spec.js diff --git a/src/state/is-within.js b/src/state/is-within.js new file mode 100644 index 0000000000..89f54f1138 --- /dev/null +++ b/src/state/is-within.js @@ -0,0 +1,6 @@ +// @flow + +// is a value between two other values + +export default (lowerBound: number, upperBound: number): ((number) => boolean) => + (value: number): boolean => value <= upperBound && value >= lowerBound; diff --git a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js index 02fcb8a82d..ce9c028a3d 100644 --- a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js +++ b/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js @@ -1,6 +1,7 @@ // @flow import { closest } from '../position'; import { droppableMapToList } from '../dimension-map-to-list'; +import isWithin from '../is-within'; import type { Axis, Position, @@ -9,9 +10,6 @@ import type { DroppableDimensionMap, } from '../../types'; -const isWithin = (lowerBound: number, upperBound: number): ((number) => boolean) => - (value: number): boolean => value <= upperBound && value >= lowerBound; - const getCorners = (droppable: DroppableDimension): Position[] => { const fragment: DimensionFragment = droppable.page.withMargin; diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index 3e35e86de2..bf8e3619a3 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -1,5 +1,6 @@ // @flow import { add, distance } from '../position'; +import isWithin from '../is-within'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import type { Axis, @@ -12,34 +13,40 @@ import type { type Args = {| axis: Axis, pageCenter: Position, - // how far the destination Droppable is scrolled - scrollOffset: Position, // the droppable that is being moved to destination: DroppableDimension, draggables: DraggableDimensionMap, |} -// TODO -const isVisible = (draggable: DraggableDimension, droppable: DroppableDimension) => true; - export default ({ axis, pageCenter, - scrollOffset, destination, draggables, }: Args): ?DraggableDimension => { - const siblings: DraggableDimension[] = getDraggablesInsideDroppable( + const options: DraggableDimension[] = getDraggablesInsideDroppable( destination, draggables ); - const result: DraggableDimension[] = - // remove any options that are hidden by overflow - siblings - .filter((draggable: DraggableDimension) => isVisible(draggable, destination)) + // Empty list - bail out + if (!options.length) { + return null; + } + + const isWithinMainAxis = isWithin( + destination.page.withMargin[axis.start], + destination.page.withMargin[axis.end] + ); + + const result: DraggableDimension[] = options + // Remove any options that are hidden by overflow + // Whole draggable must be visible to move to it + .filter((draggable: DraggableDimension) => + isWithinMainAxis(draggable.page.withMargin[axis.start]) && + isWithinMainAxis(draggable.page.withMargin[axis.end]) + ) .sort((a: DraggableDimension, b: DraggableDimension): number => { // TODO: not considering scroll offset - console.log('scroll offset', scrollOffset); const distanceToA = distance(pageCenter, a.page.withMargin.center); const distanceToB = distance(pageCenter, b.page.withMargin.center); diff --git a/test/unit/state/is-within.spec.js b/test/unit/state/is-within.spec.js new file mode 100644 index 0000000000..679e4d8a46 --- /dev/null +++ b/test/unit/state/is-within.spec.js @@ -0,0 +1,28 @@ +// @flow +import isWithin from '../../../src/state/is-within'; + +describe('is within', () => { + const lowerBound: number = 5; + const upperBound: number = 10; + const execute = isWithin(5, 10); + + it('should return true when the value is between the bounds', () => { + expect(execute(lowerBound + 1)).toBe(true); + }); + + it('should return true when the value is equal to the lower bound', () => { + expect(execute(lowerBound)).toBe(true); + }); + + it('should return true when the value is equal to the upper bound', () => { + expect(execute(upperBound)).toBe(true); + }); + + it('should return false when the value is less then the lower bound', () => { + expect(execute(lowerBound - 1)).toBe(false); + }); + + it('should return false when the value is greater than the upper bound', () => { + expect(execute(upperBound + 1)).toBe(false); + }); +}); diff --git a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js index 6a1c3d8784..74130768f4 100644 --- a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js +++ b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js @@ -1,19 +1,242 @@ // @flow +import getClosestDraggable from '../../../../src/state/move-to-best-droppable/get-closest-draggable'; +import { getDroppableDimension, getDraggableDimension } from '../../../../src/state/dimension'; +import { add, distance } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; -import type { Axis } from '../../../../src/types'; +import getClientRect from '../../../utils/get-client-rect'; +import type { + Axis, + Position, + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, +} from '../../../../src/types'; describe('get closest draggable', () => { - [horizontal, vertical].forEach((axis: Axis) => { - it('should remove draggables that are not within the current visible bounds of a droppable', () => { + const droppable: DroppableDimension = getDroppableDimension({ + id: 'droppable', + clientRect: getClientRect({ + top: 0, + left: 0, + bottom: 100, + right: 20, + }), + }); + + // first item is partially hidden on the top + const partialHiddenUpper: DraggableDimension = getDraggableDimension({ + id: 'partialHiddenUpper', + droppableId: droppable.id, + clientRect: getClientRect({ + top: -10, + left: 0, + bottom: 20, + right: 20, + }), + }); + + const visible1: DraggableDimension = getDraggableDimension({ + id: 'visible1', + droppableId: droppable.id, + clientRect: getClientRect({ + top: 20, + left: 0, + bottom: 40, + right: 20, + }), + }); + + const visible2: DraggableDimension = getDraggableDimension({ + id: 'visible2', + droppableId: droppable.id, + clientRect: getClientRect({ + top: 40, + left: 0, + // same height as visible1 + bottom: 60, + right: 20, + }), + }); + + // bleeds over the visible boundary + const partiallyHiddenLower: DraggableDimension = getDraggableDimension({ + id: 'partiallyHiddenLower', + droppableId: droppable.id, + clientRect: getClientRect({ + top: 60, + left: 0, + bottom: 120, + right: 20, + }), + }); + + // totally invisible + const hidden: DraggableDimension = getDraggableDimension({ + id: 'hidden', + droppableId: droppable.id, + clientRect: getClientRect({ + top: 90, + left: 0, + bottom: 120, + right: 20, + }), + }); + + const draggables: DraggableDimensionMap = { + [partialHiddenUpper.id]: partialHiddenUpper, + [visible1.id]: visible1, + [visible2.id]: visible2, + [partiallyHiddenLower.id]: partiallyHiddenLower, + [hidden.id]: hidden, + }; + it('should return the closest draggable', () => { + const center1: Position = { + x: 100, + y: visible1.page.withoutMargin.center.y, + }; + const result1: ?DraggableDimension = getClosestDraggable({ + axis: vertical, + pageCenter: center1, + destination: droppable, + draggables, }); + expect(result1).toBe(visible1); + + // closest to second + const center2: Position = { + x: 100, + y: visible1.page.withoutMargin.center.y, + }; + const result2: ?DraggableDimension = getClosestDraggable({ + axis: vertical, + pageCenter: center2, + destination: droppable, + draggables, + }); + expect(result2).toBe(visible1); + }); - it('should return the closest draggable', () => { + it('should return null if there are no draggables in the droppable', () => { + const center: Position = { + x: 100, + y: 100, + }; + const empty: DraggableDimensionMap = {}; + const result: ?DraggableDimension = getClosestDraggable({ + axis: vertical, + pageCenter: center, + destination: droppable, + draggables: empty, }); - it('should return the draggable that is first on the main axis in the event of tie', () => { + expect(result).toBe(null); + }); + + describe('removal of draggables that are not entirely within the current visible bounds of a droppable', () => { + it('should remove draggables that have upper partial visiblility', () => { + // point would usually be closest to first - + // but it is outside of the visible bounds of the droppable + const center: Position = { + x: 100, + y: partialHiddenUpper.page.withoutMargin.center.y, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis: vertical, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible1); + }); + + it('should remove draggables that have lower partial visiblility', () => { + const center: Position = { + x: 100, + y: partiallyHiddenLower.page.withoutMargin.center.y, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis: vertical, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible2); + }); + + it('should remove draggables that have no visiblity', () => { + const center: Position = { + x: 100, + y: hidden.page.withoutMargin.center.y, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis: vertical, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible2); + }); + + it('should return null if there are no visible targets', () => { + const notVisible: DraggableDimensionMap = { + [partialHiddenUpper.id]: partialHiddenUpper, + [partiallyHiddenLower.id]: partiallyHiddenLower, + [hidden.id]: hidden, + }; + const center: Position = { + x: 100, + y: 100, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis: vertical, + pageCenter: center, + destination: droppable, + draggables: notVisible, + }); + + expect(result).toBe(null); + }); + }); + + it('should return the draggable that is first on the main axis in the event of tie', () => { + // in this case the distance between visible1 and visible2 is the same + const center: Position = { + x: 100, + // this is a shared edge + y: visible2.page.withoutMargin.top, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis: vertical, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible1); + + // validating test assumptions + + // 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: vertical, + pageCenter: add(center, { x: 0, y: 1 }), + destination: droppable, + draggables, }); + expect(result2).toBe(visible2); }); }); From 1a09d3fc70bbd4fa925d0d9c2c63d67f066dcd92 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 14:40:19 +1000 Subject: [PATCH 069/117] adding horizontal tests to get-closest-draggable --- .../get-closest-draggable.spec.js | 554 +++++++++++++----- 1 file changed, 395 insertions(+), 159 deletions(-) diff --git a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js index 74130768f4..258fa65728 100644 --- a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js +++ b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js @@ -13,230 +13,466 @@ import type { } from '../../../../src/types'; describe('get closest draggable', () => { - const droppable: DroppableDimension = getDroppableDimension({ - id: 'droppable', - clientRect: getClientRect({ - top: 0, - left: 0, - bottom: 100, - right: 20, - }), - }); - - // first item is partially hidden on the top - const partialHiddenUpper: DraggableDimension = getDraggableDimension({ - id: 'partialHiddenUpper', - droppableId: droppable.id, - clientRect: getClientRect({ - top: -10, - left: 0, - bottom: 20, - right: 20, - }), - }); - - const visible1: DraggableDimension = getDraggableDimension({ - id: 'visible1', - droppableId: droppable.id, - clientRect: getClientRect({ - top: 20, - left: 0, - bottom: 40, - right: 20, - }), - }); + describe('on axis axis', () => { + const axis: Axis = vertical; - const visible2: DraggableDimension = getDraggableDimension({ - id: 'visible2', - droppableId: droppable.id, - clientRect: getClientRect({ - top: 40, - left: 0, - // same height as visible1 - bottom: 60, - right: 20, - }), - }); - - // bleeds over the visible boundary - const partiallyHiddenLower: DraggableDimension = getDraggableDimension({ - id: 'partiallyHiddenLower', - droppableId: droppable.id, - clientRect: getClientRect({ - top: 60, - left: 0, - bottom: 120, - right: 20, - }), - }); + const droppable: DroppableDimension = getDroppableDimension({ + id: 'droppable', + clientRect: getClientRect({ + top: 0, + left: 0, + bottom: 100, + right: 20, + }), + }); - // totally invisible - const hidden: DraggableDimension = getDraggableDimension({ - id: 'hidden', - droppableId: droppable.id, - clientRect: getClientRect({ - top: 90, - left: 0, - bottom: 120, - right: 20, - }), - }); + // first item is partially hidden on the top + const partialHiddenUpper: DraggableDimension = getDraggableDimension({ + id: 'partialHiddenUpper', + droppableId: droppable.id, + clientRect: getClientRect({ + top: -10, + left: 0, + bottom: 20, + right: 20, + }), + }); - const draggables: DraggableDimensionMap = { - [partialHiddenUpper.id]: partialHiddenUpper, - [visible1.id]: visible1, - [visible2.id]: visible2, - [partiallyHiddenLower.id]: partiallyHiddenLower, - [hidden.id]: hidden, - }; - - it('should return the closest draggable', () => { - const center1: Position = { - x: 100, - y: visible1.page.withoutMargin.center.y, - }; - const result1: ?DraggableDimension = getClosestDraggable({ - axis: vertical, - pageCenter: center1, - destination: droppable, - draggables, + const visible1: DraggableDimension = getDraggableDimension({ + id: 'visible1', + droppableId: droppable.id, + clientRect: getClientRect({ + top: 20, + left: 0, + bottom: 40, + right: 20, + }), }); - expect(result1).toBe(visible1); - // closest to second - const center2: Position = { - x: 100, - y: visible1.page.withoutMargin.center.y, - }; - const result2: ?DraggableDimension = getClosestDraggable({ - axis: vertical, - pageCenter: center2, - destination: droppable, - draggables, + const visible2: DraggableDimension = getDraggableDimension({ + id: 'visible2', + droppableId: droppable.id, + clientRect: getClientRect({ + top: 40, + left: 0, + // same height as visible1 + bottom: 60, + right: 20, + }), }); - expect(result2).toBe(visible1); - }); - it('should return null if there are no draggables in the droppable', () => { - const center: Position = { - x: 100, - y: 100, - }; - const empty: DraggableDimensionMap = {}; + // bleeds over the visible boundary + const partiallyHiddenLower: DraggableDimension = getDraggableDimension({ + id: 'partiallyHiddenLower', + droppableId: droppable.id, + clientRect: getClientRect({ + top: 60, + left: 0, + bottom: 120, + right: 20, + }), + }); - const result: ?DraggableDimension = getClosestDraggable({ - axis: vertical, - pageCenter: center, - destination: droppable, - draggables: empty, + // totally invisible + const hidden: DraggableDimension = getDraggableDimension({ + id: 'hidden', + droppableId: droppable.id, + clientRect: getClientRect({ + top: 90, + left: 0, + bottom: 120, + right: 20, + }), }); - expect(result).toBe(null); - }); + const draggables: DraggableDimensionMap = { + [partialHiddenUpper.id]: partialHiddenUpper, + [visible1.id]: visible1, + [visible2.id]: visible2, + [partiallyHiddenLower.id]: partiallyHiddenLower, + [hidden.id]: hidden, + }; - describe('removal of draggables that are not entirely within the current visible bounds of a droppable', () => { - it('should remove draggables that have upper partial visiblility', () => { - // point would usually be closest to first - - // but it is outside of the visible bounds of the droppable - const center: Position = { + it('should return the closest draggable', () => { + const center1: Position = { x: 100, - y: partialHiddenUpper.page.withoutMargin.center.y, + y: visible1.page.withoutMargin.center.y, }; - - const result: ?DraggableDimension = getClosestDraggable({ - axis: vertical, - pageCenter: center, + const result1: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center1, destination: droppable, draggables, }); + expect(result1).toBe(visible1); - expect(result).toBe(visible1); + // closest to second + const center2: Position = { + x: 100, + y: visible1.page.withoutMargin.center.y, + }; + const result2: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center2, + destination: droppable, + draggables, + }); + expect(result2).toBe(visible1); }); - it('should remove draggables that have lower partial visiblility', () => { + it('should return null if there are no draggables in the droppable', () => { const center: Position = { x: 100, - y: partiallyHiddenLower.page.withoutMargin.center.y, + y: 100, }; + const empty: DraggableDimensionMap = {}; const result: ?DraggableDimension = getClosestDraggable({ - axis: vertical, + axis, pageCenter: center, destination: droppable, - draggables, + draggables: empty, }); - expect(result).toBe(visible2); + expect(result).toBe(null); }); - it('should remove draggables that have no visiblity', () => { + describe('removal of draggables that are not entirely within the current visible bounds of a droppable', () => { + it('should remove draggables that have upper partial visiblility', () => { + // point would usually be closest to first - + // but it is outside of the visible bounds of the droppable + const center: Position = { + x: 100, + y: partialHiddenUpper.page.withoutMargin.center.y, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible1); + }); + + it('should remove draggables that have lower partial visiblility', () => { + const center: Position = { + x: 100, + y: partiallyHiddenLower.page.withoutMargin.center.y, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible2); + }); + + it('should remove draggables that have no visiblity', () => { + const center: Position = { + x: 100, + y: hidden.page.withoutMargin.center.y, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible2); + }); + + it('should return null if there are no visible targets', () => { + const notVisible: DraggableDimensionMap = { + [partialHiddenUpper.id]: partialHiddenUpper, + [partiallyHiddenLower.id]: partiallyHiddenLower, + [hidden.id]: hidden, + }; + const center: Position = { + x: 100, + y: 100, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + draggables: notVisible, + }); + + 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 = { x: 100, - y: hidden.page.withoutMargin.center.y, + // this is a shared edge + y: visible2.page.withoutMargin.top, }; const result: ?DraggableDimension = getClosestDraggable({ - axis: vertical, + axis, pageCenter: center, destination: droppable, draggables, }); - expect(result).toBe(visible2); + expect(result).toBe(visible1); + + // validating test assumptions + + // 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, { x: 0, y: 1 }), + destination: droppable, + draggables, + }); + expect(result2).toBe(visible2); }); + }); - it('should return null if there are no visible targets', () => { - const notVisible: DraggableDimensionMap = { - [partialHiddenUpper.id]: partialHiddenUpper, - [partiallyHiddenLower.id]: partiallyHiddenLower, - [hidden.id]: hidden, + describe('on horizontal axis', () => { + const axis: Axis = horizontal; + const bottom: number = 20; + const top: number = 10; + + const droppable: DroppableDimension = getDroppableDimension({ + id: 'droppable', + clientRect: getClientRect({ + top, + bottom, + left: 0, + right: 100, + }), + }); + + // first item is partially hidden on the top + const partialHiddenBackwards: DraggableDimension = getDraggableDimension({ + id: 'partialHiddenBackwards', + droppableId: droppable.id, + clientRect: getClientRect({ + top, + bottom, + left: -10, + right: 20, + }), + }); + + const visible1: DraggableDimension = getDraggableDimension({ + id: 'visible1', + droppableId: droppable.id, + clientRect: getClientRect({ + top, + bottom, + left: 20, + right: 40, + }), + }); + + const visible2: DraggableDimension = getDraggableDimension({ + id: 'visible2', + droppableId: droppable.id, + clientRect: getClientRect({ + top, + bottom, + left: 40, + right: 60, + }), + }); + + // bleeds over the visible boundary + const partiallyHiddenForward: DraggableDimension = getDraggableDimension({ + id: 'partiallyHiddenForward', + droppableId: droppable.id, + clientRect: getClientRect({ + top, + bottom, + left: 60, + right: 120, + }), + }); + + // totally invisible + const hidden: DraggableDimension = getDraggableDimension({ + id: 'hidden', + droppableId: droppable.id, + clientRect: getClientRect({ + top, + bottom, + left: 120, + right: 140, + }), + }); + + const draggables: DraggableDimensionMap = { + [partialHiddenBackwards.id]: partialHiddenBackwards, + [visible1.id]: visible1, + [visible2.id]: visible2, + [partiallyHiddenForward.id]: partiallyHiddenForward, + [hidden.id]: hidden, + }; + + it('should return the closest draggable', () => { + const center1: Position = { + y: 100, + x: visible1.page.withoutMargin.center.x, + }; + const result1: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center1, + destination: droppable, + draggables, + }); + expect(result1).toBe(visible1); + + // closest to second + const center2: Position = { + y: 100, + x: visible1.page.withoutMargin.center.x, }; + const result2: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center2, + destination: droppable, + draggables, + }); + expect(result2).toBe(visible1); + }); + + it('should return null if there are no draggables in the droppable', () => { const center: Position = { x: 100, y: 100, }; + const empty: DraggableDimensionMap = {}; const result: ?DraggableDimension = getClosestDraggable({ - axis: vertical, + axis, pageCenter: center, destination: droppable, - draggables: notVisible, + draggables: empty, }); expect(result).toBe(null); }); - }); - it('should return the draggable that is first on the main axis in the event of tie', () => { - // in this case the distance between visible1 and visible2 is the same - const center: Position = { - x: 100, - // this is a shared edge - y: visible2.page.withoutMargin.top, - }; + describe('removal of draggables that are not entirely within the current visible bounds of a droppable', () => { + it('should remove draggables that have backwards partial visiblility', () => { + // point would usually be closest to first - + // but it is outside of the visible bounds of the droppable + const center: Position = { + y: 100, + x: partialHiddenBackwards.page.withoutMargin.center.x, + }; - const result: ?DraggableDimension = getClosestDraggable({ - axis: vertical, - pageCenter: center, - destination: droppable, - draggables, + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible1); + }); + + it('should remove draggables that have forward partial visiblility', () => { + const center: Position = { + y: 100, + x: partiallyHiddenForward.page.withoutMargin.center.x, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible2); + }); + + it('should remove draggables that have no visiblity', () => { + const center: Position = { + y: 100, + x: hidden.page.withoutMargin.center.x, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible2); + }); + + it('should return null if there are no visible targets', () => { + const notVisible: DraggableDimensionMap = { + [partialHiddenBackwards.id]: partialHiddenBackwards, + [partiallyHiddenForward.id]: partiallyHiddenForward, + [hidden.id]: hidden, + }; + const center: Position = { + x: 100, + y: 100, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + draggables: notVisible, + }); + + expect(result).toBe(null); + }); }); - expect(result).toBe(visible1); + 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 = { + y: 100, + // this is a shared edge + x: visible2.page.withoutMargin.left, + }; + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: droppable, + draggables, + }); + + expect(result).toBe(visible1); // validating test assumptions // 1. that they have equal distances - expect(distance(center, visible1.page.withoutMargin.center)) + 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: vertical, - pageCenter: add(center, { x: 0, y: 1 }), - destination: droppable, - draggables, + // 2. if we move beyond the edge visible2 will be selected + const result2: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: add(center, { x: 1, y: 0 }), + destination: droppable, + draggables, + }); + expect(result2).toBe(visible2); }); - expect(result2).toBe(visible2); }); }); From 3e13b31f94bcda46d29e2fc148fceb55f29bc528 Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Fri, 8 Sep 2017 14:47:23 +1000 Subject: [PATCH 070/117] Jaredcrowe/moving lists disabled droppables (#80) * isDropDisabled prop on droppables is now respected during a drag. isDropDisabled prop can be changed dynamically mid-drag. * add comment RE disabled droppables to getDragImpact * adding tests for get-closest-draggable * add error checking to UPDATE_DROPPABLE_IS_ENABLED reducer * adding horizontal tests to get-closest-draggable * isDropDisabled prop on droppables is now respected during a drag. isDropDisabled prop can be changed dynamically mid-drag. * add comment RE disabled droppables to getDragImpact * add error checking to UPDATE_DROPPABLE_IS_ENABLED reducer --- src/state/action-creators.js | 17 +++++++++ src/state/get-drag-impact.js | 38 ++++++++++++++++++- src/state/no-impact.js | 2 +- src/state/reducer.js | 35 +++++++++++++++++ ...connected-droppable-dimension-publisher.js | 2 + .../droppable-dimension-publisher-types.js | 2 + .../droppable-dimension-publisher.jsx | 7 +++- src/view/droppable/droppable.jsx | 19 +++++++--- stories/5-multiple-vertical-lists-story.js | 4 +- stories/src/multiple-vertical/quote-app.jsx | 38 +++++++++++++++---- stories/src/multiple-vertical/quote-list.jsx | 15 +++++--- 11 files changed, 155 insertions(+), 24 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index f8419e91ae..38d7304d5c 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -130,6 +130,23 @@ export const updateDroppableDimensionScroll = }, }); +export type UpdateDroppableIsEnabledAction = {| + type: 'UPDATE_DROPPABLE_IS_ENABLED', + payload: { + id: DroppableId, + isEnabled: boolean, + } +|} + +export const updateDroppableIsEnabled = + (id: DroppableId, isEnabled: boolean): UpdateDroppableIsEnabledAction => ({ + type: 'UPDATE_DROPPABLE_IS_ENABLED', + payload: { + id, + isEnabled, + }, + }); + export type MoveAction = {| type: 'MOVE', payload: {| diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 529bedc608..36505fe350 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -15,7 +15,7 @@ import type { DraggableId, import { patch } from './position'; import getDroppableOver from './get-droppable-over'; import getDraggablesInsideDroppable from './get-draggables-inside-droppable'; -import noImpact from './no-impact'; +import noImpact, { noMovement } from './no-impact'; // Calculates the net scroll diff along the main axis // between two droppables with internal scrolling @@ -76,6 +76,42 @@ export default ({ draggables, ); + // If the droppable is disabled we still need to return an impact with a destination, otherwise + // we'll get errors when trying to lift from a disabled droppable (which is allowed) + if (!droppable.isEnabled) { + const homeDroppableId = draggingDimension.droppableId; + const homeIndex = getDraggablesInsideDroppable( + droppables[homeDroppableId], + draggables, + ).indexOf(draggingDimension); + return { + movement: noMovement, + direction: null, + destination: { + droppableId: homeDroppableId, + index: homeIndex, + }, + }; + } + + // If the droppable is disabled we still need to return an impact with a destination, otherwise + // we'll get errors when trying to lift from a disabled droppable (which is allowed) + if (!droppable.isEnabled) { + const homeDroppableId = draggingDimension.droppableId; + const homeIndex = getDraggablesInsideDroppable( + droppables[homeDroppableId], + draggables, + ).indexOf(draggingDimension); + return { + movement: noMovement, + direction: null, + destination: { + droppableId: homeDroppableId, + index: homeIndex, + }, + }; + } + // not considering margin so that items move based on visible edges const draggableCenter: Position = draggingDimension.page.withoutMargin.center; const isBeyondStartPosition: boolean = newCenter[axis.line] - draggableCenter[axis.line] > 0; diff --git a/src/state/no-impact.js b/src/state/no-impact.js index 91fae6c597..e9d44c3ad6 100644 --- a/src/state/no-impact.js +++ b/src/state/no-impact.js @@ -3,7 +3,7 @@ import type { DragMovement, DragImpact, Position } from '../types'; const origin: Position = { x: 0, y: 0 }; -const noMovement: DragMovement = { +export const noMovement: DragMovement = { draggables: [], amount: origin, isBeyondStartPosition: false, diff --git a/src/state/reducer.js b/src/state/reducer.js index d9f3e1a5b5..1c675fe695 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -339,6 +339,41 @@ export default (state: State = clean('IDLE'), action: Action): State => { }); } + if (action.type === 'UPDATE_DROPPABLE_IS_ENABLED') { + if (!Object.keys(state.dimension.droppable).length) { + return state; + } + + const { id, isEnabled } = action.payload; + const target = state.dimension.droppable[id]; + + if (!target) { + console.error('cannot update enabled flag on droppable that does not have a dimension'); + return clean(); + } + + if (target.isEnabled === isEnabled) { + console.warn(`trying to set droppable isEnabled to ${isEnabled} but it is already ${isEnabled}`); + return state; + } + + const updatedDroppableDimension = { + ...target, + isEnabled, + }; + + return { + ...state, + dimension: { + ...state.dimension, + droppable: { + ...state.dimension.droppable, + [id]: updatedDroppableDimension, + }, + }, + }; + } + if (action.type === 'MOVE') { const { client, page, windowScroll } = action.payload; return move({ diff --git a/src/view/droppable-dimension-publisher/connected-droppable-dimension-publisher.js b/src/view/droppable-dimension-publisher/connected-droppable-dimension-publisher.js index 092f6b6d9e..1389641bbe 100644 --- a/src/view/droppable-dimension-publisher/connected-droppable-dimension-publisher.js +++ b/src/view/droppable-dimension-publisher/connected-droppable-dimension-publisher.js @@ -13,6 +13,7 @@ import { storeKey } from '../context-keys'; import DroppableDimensionPublisher from './droppable-dimension-publisher'; import { publishDroppableDimension, + updateDroppableIsEnabled, updateDroppableDimensionScroll, } from '../../state/action-creators'; @@ -43,6 +44,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps: DispatchProps = { publish: publishDroppableDimension, updateScroll: updateDroppableDimensionScroll, + updateIsEnabled: updateDroppableIsEnabled, }; export default connect( diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js b/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js index cfa53daf5d..58ad949bc1 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js @@ -15,12 +15,14 @@ export type MapProps = {| export type DispatchProps = {| publish: (dimension: DroppableDimension) => mixed, + updateIsEnabled: (id: DroppableId, isEnabled: boolean) => mixed, updateScroll: (id: DroppableId, offset: Position) => mixed, |} export type OwnProps = {| droppableId: DroppableId, direction: Direction, + isDropDisabled: boolean, type: TypeId, targetRef: ?HTMLElement, children?: ReactElement, diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 84a11ab5be..993aa122fa 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -34,7 +34,7 @@ export default class DroppableDimensionPublisher extends Component { } getDimension = (): DroppableDimension => { - const { droppableId, direction, targetRef } = this.props; + const { droppableId, direction, isDropDisabled, targetRef } = this.props; invariant(targetRef, 'DimensionPublisher cannot calculate a dimension when not attached to the DOM'); const style = window.getComputedStyle(targetRef); @@ -53,6 +53,7 @@ export default class DroppableDimensionPublisher extends Component { margin, windowScroll: getWindowScrollPosition(), scroll: this.getScrollOffset(), + isEnabled: !isDropDisabled, }); return dimension; @@ -114,6 +115,10 @@ export default class DroppableDimensionPublisher extends Component { } } + if (nextProps.isDropDisabled !== this.props.isDropDisabled) { + this.props.updateIsEnabled(this.props.droppableId, !nextProps.isDropDisabled); + } + // Because the dimension publisher wraps children - it might render even when its props do // not change. We need to ensure that it does not publish when it should not. const shouldPublish = !this.props.shouldPublish && nextProps.shouldPublish; diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 95bf4cbf9e..61c73495a5 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -75,22 +75,31 @@ export default class Droppable extends Component { } render() { + const { + children, + direction, + droppableId, + isDraggingOver, + isDropDisabled, + type, + } = this.props; const provided: Provided = { innerRef: this.setRef, placeholder: this.getPlaceholder(), }; const snapshot: StateSnapshot = { - isDraggingOver: this.props.isDraggingOver, + isDraggingOver, }; return ( - {this.props.children(provided, snapshot)} + {children(provided, snapshot)} ); } diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js index 51683b2ec8..142c60cb3f 100644 --- a/stories/5-multiple-vertical-lists-story.js +++ b/stories/5-multiple-vertical-lists-story.js @@ -12,9 +12,9 @@ const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( ); const initialQuotes = { - alpha: namespaceQuoteIds(getQuotes(20), 'alpha'), + alpha: namespaceQuoteIds(getQuotes(8), 'alpha'), beta: namespaceQuoteIds(getQuotes(3), 'beta'), - gamma: namespaceQuoteIds(getQuotes(10), 'gamma'), + gamma: namespaceQuoteIds(getQuotes(6), 'gamma'), delta: namespaceQuoteIds(getQuotes(0), 'delta'), }; diff --git a/stories/src/multiple-vertical/quote-app.jsx b/stories/src/multiple-vertical/quote-app.jsx index a856ea12e2..9b48763e58 100644 --- a/stories/src/multiple-vertical/quote-app.jsx +++ b/stories/src/multiple-vertical/quote-app.jsx @@ -45,6 +45,7 @@ type Props = {| |} type State = {| + disabledDroppable: ?string, quotes: GroupedQuotes, |} @@ -86,12 +87,16 @@ export default class QuoteApp extends Component { state: State state: State = { + disabledDroppable: null, quotes: this.props.initial, }; /* eslint-enable react/sort-comp */ onDragStart = (initial: DragStart) => { publishOnDragStart(initial); + this.setState({ + disabledDroppable: this.getDisabledDroppable(initial.source.droppableId), + }); // $ExpectError - body could be null? document.body.classList.add(isDraggingClassName); } @@ -101,14 +106,16 @@ export default class QuoteApp extends Component { // $ExpectError - body could be null? document.body.classList.remove(isDraggingClassName); - // // dropped outside the list - if (!result.destination) { - return; - } - - const quotes = resolveDrop(this.state.quotes, result.source, result.destination); + const quotes = result.destination ? ( + resolveDrop(this.state.quotes, result.source, result.destination) + ) : ( + this.state.quotes + ); - this.setState({ quotes }); + this.setState({ + disabledDroppable: null, + quotes, + }); } componentDidMount() { @@ -121,8 +128,19 @@ export default class QuoteApp extends Component { `; } + getDisabledDroppable = (sourceDroppable: ?string) => { + if (!sourceDroppable) { + return null; + } + const droppables: string[] = ['alpha', 'beta', 'gamma', 'delta']; + const sourceIndex = droppables.indexOf(sourceDroppable); + const disabledDroppableIndex = (sourceIndex + 1) % droppables.length; + + return droppables[disabledDroppableIndex]; + } + render() { - const { quotes } = this.state; + const { quotes, disabledDroppable } = this.state; return ( @@ -142,12 +161,14 @@ export default class QuoteApp extends Component { @@ -156,6 +177,7 @@ export default class QuoteApp extends Component { listId="delta" listType="card" internalScroll + isDropDisabled={disabledDroppable === 'delta'} quotes={quotes.delta} /> diff --git a/stories/src/multiple-vertical/quote-list.jsx b/stories/src/multiple-vertical/quote-list.jsx index a6fcbce604..99b55aa4f3 100644 --- a/stories/src/multiple-vertical/quote-list.jsx +++ b/stories/src/multiple-vertical/quote-list.jsx @@ -13,14 +13,15 @@ import type { } from '../../../src/'; const Wrapper = styled.div` - width: 250px; background-color: ${({ isDraggingOver }) => (isDraggingOver ? colors.blue.lighter : colors.blue.light)}; display: flex; flex-direction: column; + opacity: ${({ isDropDisabled }) => (isDropDisabled ? 0.5 : 1)}; padding: ${grid}px; padding-bottom: 0; + transition: background-color 0.1s ease, opacity 0.1s ease; user-select: none; - transition: background-color 0.1s ease; + width: 250px; `; const DropZone = styled.div` @@ -49,10 +50,11 @@ const Title = styled.h4` export default class QuoteList extends Component { props: {| listId: string, - quotes: Quote[], listType?: string, + internalScroll?: boolean, + isDropDisabled?: boolean, + quotes: Quote[], style?: Object, - internalScroll ?: boolean, |} renderBoard = (dropProvided: DroppableProvided) => { @@ -84,14 +86,15 @@ export default class QuoteList extends Component { } render() { - const { listId, listType, internalScroll, style } = this.props; + const { listId, listType, internalScroll, isDropDisabled, style } = this.props; return ( - + {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( {internalScroll ? ( From 3fb9a5f3c494521a73c0b311bf40f09ebd433838 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 15:04:46 +1000 Subject: [PATCH 071/117] get-closest-draggable now uses a loop to check axis --- .../get-closest-draggable.spec.js | 369 ++++-------------- 1 file changed, 68 insertions(+), 301 deletions(-) diff --git a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js index 258fa65728..5969cc61de 100644 --- a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js +++ b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js @@ -1,7 +1,7 @@ // @flow import getClosestDraggable from '../../../../src/state/move-to-best-droppable/get-closest-draggable'; import { getDroppableDimension, getDraggableDimension } from '../../../../src/state/dimension'; -import { add, distance } from '../../../../src/state/position'; +import { add, distance, patch } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; import getClientRect from '../../../utils/get-client-rect'; import type { @@ -13,261 +13,31 @@ import type { } from '../../../../src/types'; describe('get closest draggable', () => { - describe('on axis axis', () => { - const axis: Axis = vertical; + [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({ id: 'droppable', clientRect: getClientRect({ - top: 0, - left: 0, - bottom: 100, - right: 20, + [axis.start]: start, + [axis.end]: end, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, }), }); - // first item is partially hidden on the top - const partialHiddenUpper: DraggableDimension = getDraggableDimension({ - id: 'partialHiddenUpper', - droppableId: droppable.id, - clientRect: getClientRect({ - top: -10, - left: 0, - bottom: 20, - right: 20, - }), - }); - - const visible1: DraggableDimension = getDraggableDimension({ - id: 'visible1', - droppableId: droppable.id, - clientRect: getClientRect({ - top: 20, - left: 0, - bottom: 40, - right: 20, - }), - }); - - const visible2: DraggableDimension = getDraggableDimension({ - id: 'visible2', - droppableId: droppable.id, - clientRect: getClientRect({ - top: 40, - left: 0, - // same height as visible1 - bottom: 60, - right: 20, - }), - }); - - // bleeds over the visible boundary - const partiallyHiddenLower: DraggableDimension = getDraggableDimension({ - id: 'partiallyHiddenLower', - droppableId: droppable.id, - clientRect: getClientRect({ - top: 60, - left: 0, - bottom: 120, - right: 20, - }), - }); - - // totally invisible - const hidden: DraggableDimension = getDraggableDimension({ - id: 'hidden', - droppableId: droppable.id, - clientRect: getClientRect({ - top: 90, - left: 0, - bottom: 120, - right: 20, - }), - }); - - const draggables: DraggableDimensionMap = { - [partialHiddenUpper.id]: partialHiddenUpper, - [visible1.id]: visible1, - [visible2.id]: visible2, - [partiallyHiddenLower.id]: partiallyHiddenLower, - [hidden.id]: hidden, - }; - - it('should return the closest draggable', () => { - const center1: Position = { - x: 100, - y: visible1.page.withoutMargin.center.y, - }; - const result1: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center1, - destination: droppable, - draggables, - }); - expect(result1).toBe(visible1); - - // closest to second - const center2: Position = { - x: 100, - y: visible1.page.withoutMargin.center.y, - }; - const result2: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center2, - destination: droppable, - draggables, - }); - expect(result2).toBe(visible1); - }); - - it('should return null if there are no draggables in the droppable', () => { - const center: Position = { - x: 100, - y: 100, - }; - const empty: DraggableDimensionMap = {}; - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: droppable, - draggables: empty, - }); - - expect(result).toBe(null); - }); - - describe('removal of draggables that are not entirely within the current visible bounds of a droppable', () => { - it('should remove draggables that have upper partial visiblility', () => { - // point would usually be closest to first - - // but it is outside of the visible bounds of the droppable - const center: Position = { - x: 100, - y: partialHiddenUpper.page.withoutMargin.center.y, - }; - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: droppable, - draggables, - }); - - expect(result).toBe(visible1); - }); - - it('should remove draggables that have lower partial visiblility', () => { - const center: Position = { - x: 100, - y: partiallyHiddenLower.page.withoutMargin.center.y, - }; - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: droppable, - draggables, - }); - - expect(result).toBe(visible2); - }); - - it('should remove draggables that have no visiblity', () => { - const center: Position = { - x: 100, - y: hidden.page.withoutMargin.center.y, - }; - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: droppable, - draggables, - }); - - expect(result).toBe(visible2); - }); - - it('should return null if there are no visible targets', () => { - const notVisible: DraggableDimensionMap = { - [partialHiddenUpper.id]: partialHiddenUpper, - [partiallyHiddenLower.id]: partiallyHiddenLower, - [hidden.id]: hidden, - }; - const center: Position = { - x: 100, - y: 100, - }; - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: droppable, - draggables: notVisible, - }); - - 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 = { - x: 100, - // this is a shared edge - y: visible2.page.withoutMargin.top, - }; - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: droppable, - draggables, - }); - - expect(result).toBe(visible1); - - // validating test assumptions - - // 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, { x: 0, y: 1 }), - destination: droppable, - draggables, - }); - expect(result2).toBe(visible2); - }); - }); - - describe('on horizontal axis', () => { - const axis: Axis = horizontal; - const bottom: number = 20; - const top: number = 10; - - const droppable: DroppableDimension = getDroppableDimension({ - id: 'droppable', - clientRect: getClientRect({ - top, - bottom, - left: 0, - right: 100, - }), - }); - - // first item is partially hidden on the top + // first item bleeds backwards past the start of the droppable const partialHiddenBackwards: DraggableDimension = getDraggableDimension({ - id: 'partialHiddenBackwards', + id: 'bleedsOverStart', droppableId: droppable.id, clientRect: getClientRect({ - top, - bottom, - left: -10, - right: 20, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: -10, // -10 + [axis.end]: 20, }), }); @@ -275,10 +45,10 @@ describe('get closest draggable', () => { id: 'visible1', droppableId: droppable.id, clientRect: getClientRect({ - top, - bottom, - left: 20, - right: 40, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 20, + [axis.end]: 40, }), }); @@ -286,22 +56,22 @@ describe('get closest draggable', () => { id: 'visible2', droppableId: droppable.id, clientRect: getClientRect({ - top, - bottom, - left: 40, - right: 60, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 40, + [axis.end]: 60, }), }); - // bleeds over the visible boundary - const partiallyHiddenForward: DraggableDimension = getDraggableDimension({ - id: 'partiallyHiddenForward', + // bleeds over the end of the visible boundary + const partiallyHiddenForwards: DraggableDimension = getDraggableDimension({ + id: 'bleedsOverEnd', droppableId: droppable.id, clientRect: getClientRect({ - top, - bottom, - left: 60, - right: 120, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 60, + [axis.end]: 120, }), }); @@ -310,10 +80,10 @@ describe('get closest draggable', () => { id: 'hidden', droppableId: droppable.id, clientRect: getClientRect({ - top, - bottom, - left: 120, - right: 140, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 120, + [axis.end]: 140, }), }); @@ -321,15 +91,15 @@ describe('get closest draggable', () => { [partialHiddenBackwards.id]: partialHiddenBackwards, [visible1.id]: visible1, [visible2.id]: visible2, - [partiallyHiddenForward.id]: partiallyHiddenForward, + [partiallyHiddenForwards.id]: partiallyHiddenForwards, [hidden.id]: hidden, }; it('should return the closest draggable', () => { - const center1: Position = { - y: 100, - x: visible1.page.withoutMargin.center.x, - }; + // closet to visible1 + const center1: Position = patch( + axis.line, visible1.page.withoutMargin.center[axis.line], 100 + ); const result1: ?DraggableDimension = getClosestDraggable({ axis, pageCenter: center1, @@ -338,18 +108,17 @@ describe('get closest draggable', () => { }); expect(result1).toBe(visible1); - // closest to second - const center2: Position = { - y: 100, - x: visible1.page.withoutMargin.center.x, - }; + // 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, draggables, }); - expect(result2).toBe(visible1); + expect(result2).toBe(visible2); }); it('should return null if there are no draggables in the droppable', () => { @@ -371,12 +140,11 @@ describe('get closest draggable', () => { describe('removal of draggables that are not entirely within the current visible bounds of a droppable', () => { it('should remove draggables that have backwards partial visiblility', () => { - // point would usually be closest to first - - // but it is outside of the visible bounds of the droppable - const center: Position = { - y: 100, - x: partialHiddenBackwards.page.withoutMargin.center.x, - }; + // point would usually be closest to visible1 - + // but it is outside of the visible bounds of the droppable + const center: Position = patch( + axis.line, partialHiddenBackwards.page.withoutMargin.center[axis.line], 100 + ); const result: ?DraggableDimension = getClosestDraggable({ axis, @@ -389,10 +157,9 @@ describe('get closest draggable', () => { }); it('should remove draggables that have forward partial visiblility', () => { - const center: Position = { - y: 100, - x: partiallyHiddenForward.page.withoutMargin.center.x, - }; + const center: Position = patch( + axis.line, partiallyHiddenForwards.page.withoutMargin.center[axis.line], 100 + ); const result: ?DraggableDimension = getClosestDraggable({ axis, @@ -405,10 +172,9 @@ describe('get closest draggable', () => { }); it('should remove draggables that have no visiblity', () => { - const center: Position = { - y: 100, - x: hidden.page.withoutMargin.center.x, - }; + const center: Position = patch( + axis.line, hidden.page.withoutMargin.center[axis.line], 100 + ); const result: ?DraggableDimension = getClosestDraggable({ axis, @@ -423,12 +189,12 @@ describe('get closest draggable', () => { it('should return null if there are no visible targets', () => { const notVisible: DraggableDimensionMap = { [partialHiddenBackwards.id]: partialHiddenBackwards, - [partiallyHiddenForward.id]: partiallyHiddenForward, + [partiallyHiddenForwards.id]: partiallyHiddenForwards, [hidden.id]: hidden, }; const center: Position = { - x: 100, - y: 100, + x: 0, + y: 0, }; const result: ?DraggableDimension = getClosestDraggable({ @@ -443,12 +209,13 @@ describe('get closest draggable', () => { }); 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 = { - y: 100, - // this is a shared edge - x: visible2.page.withoutMargin.left, - }; + // 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, @@ -461,14 +228,14 @@ describe('get closest draggable', () => { // validating test assumptions - // 1. that they have equal distances + // 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, { x: 1, y: 0 }), + pageCenter: add(center, patch(axis.line, 1)), destination: droppable, draggables, }); From 41e9ebcd8376132c17fd0c870af37ee2b5ba5271 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 16:00:02 +1000 Subject: [PATCH 072/117] get-closest-draggable now considers the droppables scroll --- .../get-closest-draggable.js | 6 +- .../get-closest-draggable.spec.js | 65 +++++++++++++++++-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index bf8e3619a3..d5936a843d 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -33,9 +33,11 @@ export default ({ return null; } + const containerScroll: Position = destination.scroll.current; + const isWithinMainAxis = isWithin( - destination.page.withMargin[axis.start], - destination.page.withMargin[axis.end] + destination.page.withMargin[axis.start] + containerScroll[axis.line], + destination.page.withMargin[axis.end] + containerScroll[axis.line] ); const result: DraggableDimension[] = options diff --git a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js index 5969cc61de..71ac4af7a7 100644 --- a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js +++ b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js @@ -12,6 +12,19 @@ import type { DraggableDimensionMap, } from '../../../../src/types'; +const withScroll = (droppable: DroppableDimension, scroll: Position): DroppableDimension => { + // $ExpectError - using spread + const updated: DroppableDimension = { + ...droppable, + scroll: { + initial: droppable.scroll.initial, + current: scroll, + }, + }; + + return updated; +}; + describe('get closest draggable', () => { [vertical, horizontal].forEach((axis: Axis) => { const start: number = 0; @@ -31,7 +44,7 @@ describe('get closest draggable', () => { // first item bleeds backwards past the start of the droppable const partialHiddenBackwards: DraggableDimension = getDraggableDimension({ - id: 'bleedsOverStart', + id: 'partialHiddenBackwards', droppableId: droppable.id, clientRect: getClientRect({ [axis.crossAxisStart]: crossAxisStart, @@ -65,7 +78,7 @@ describe('get closest draggable', () => { // bleeds over the end of the visible boundary const partiallyHiddenForwards: DraggableDimension = getDraggableDimension({ - id: 'bleedsOverEnd', + id: 'partiallyHiddenForwards', droppableId: droppable.id, clientRect: getClientRect({ [axis.crossAxisStart]: crossAxisStart, @@ -139,7 +152,7 @@ describe('get closest draggable', () => { }); describe('removal of draggables that are not entirely within the current visible bounds of a droppable', () => { - it('should remove draggables that have backwards partial visiblility', () => { + it('should ignore draggables that have backwards partial visiblility', () => { // point would usually be closest to visible1 - // but it is outside of the visible bounds of the droppable const center: Position = patch( @@ -156,7 +169,7 @@ describe('get closest draggable', () => { expect(result).toBe(visible1); }); - it('should remove draggables that have forward partial visiblility', () => { + it('should ignore draggables that have forward partial visiblility', () => { const center: Position = patch( axis.line, partiallyHiddenForwards.page.withoutMargin.center[axis.line], 100 ); @@ -171,7 +184,7 @@ describe('get closest draggable', () => { expect(result).toBe(visible2); }); - it('should remove draggables that have no visiblity', () => { + it('should ignore draggables that have no visiblity', () => { const center: Position = patch( axis.line, hidden.page.withoutMargin.center[axis.line], 100 ); @@ -206,6 +219,48 @@ describe('get closest draggable', () => { expect(result).toBe(null); }); + + describe('taking into account droppable scroll', () => { + it('should include forward items that otherwise would be excluded', () => { + const center: Position = patch( + axis.line, partiallyHiddenForwards.page.withoutMargin.center[axis.line], 100 + ); + // need to scroll so that partiallyHiddenForwards is now visible + const diffToEnd: number = + partiallyHiddenForwards.page.withoutMargin[axis.end] - + droppable.page.withoutMargin[axis.end]; + + const scroll: Position = patch(axis.line, diffToEnd); + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: withScroll(droppable, scroll), + draggables, + }); + + expect(result).toBe(partiallyHiddenForwards); + }); + + it.only('should include backward items that otherwise would be excluded', () => { + const center: Position = patch( + axis.line, partialHiddenBackwards.page.withoutMargin.center[axis.line], 100 + ); + // need to scroll so that partialHiddenBackwards is now visible + const diffToEnd: number = partialHiddenBackwards.page.withoutMargin[axis.start] - + droppable.page.withoutMargin[axis.start]; + const scroll: Position = patch(axis.line, diffToEnd); + + const result: ?DraggableDimension = getClosestDraggable({ + axis, + pageCenter: center, + destination: withScroll(droppable, scroll), + draggables, + }); + + expect(result).toBe(partialHiddenBackwards); + }); + }); }); it('should return the draggable that is first on the main axis in the event of a tie', () => { From c59e57ba8cec28798edeee691b0f2129ef1025cb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 20:24:20 +1000 Subject: [PATCH 073/117] container scroll now cancels a keyboard drag --- src/state/get-droppable-over.js | 14 +------ .../is-within-visible-bounds-of-droppable.js | 17 ++++++++ .../get-closest-draggable.js | 8 ++-- src/state/move-to-best-droppable/index.js | 9 ++--- src/state/reducer.js | 39 +++++++++++-------- .../droppable-dimension-publisher.jsx | 35 ++++++++++++++++- stories/5-multiple-vertical-lists-story.js | 4 +- stories/src/multiple-vertical/quote-app.jsx | 1 + stories/src/multiple-vertical/quote-list.jsx | 2 +- 9 files changed, 85 insertions(+), 44 deletions(-) create mode 100644 src/state/is-within-visible-bounds-of-droppable.js diff --git a/src/state/get-droppable-over.js b/src/state/get-droppable-over.js index b74504c080..03cb5ee206 100644 --- a/src/state/get-droppable-over.js +++ b/src/state/get-droppable-over.js @@ -1,30 +1,20 @@ // @flow import { droppableMapToList } from './dimension-map-to-list'; +import isWithinVisibleBoundsOfDroppable from './is-within-visible-bounds-of-droppable'; import type { DroppableId, Position, DroppableDimensionMap, DroppableDimension, - DimensionFragment, } from '../types'; -const isOverDroppable = (target: Position, droppable: DroppableDimension): boolean => { - const fragment: DimensionFragment = droppable.page.withMargin; - const { top, right, bottom, left } = fragment; - - return target.x >= left && - target.x <= right && - target.y >= top && - target.y <= bottom; -}; - export default ( target: Position, droppables: DroppableDimensionMap, ): ?DroppableId => { const maybe: ?DroppableDimension = droppableMapToList(droppables) .find((droppable: DroppableDimension): boolean => ( - isOverDroppable(target, droppable) + isWithinVisibleBoundsOfDroppable(target, droppable) )); return maybe ? maybe.id : null; diff --git a/src/state/is-within-visible-bounds-of-droppable.js b/src/state/is-within-visible-bounds-of-droppable.js new file mode 100644 index 0000000000..77d5c6108a --- /dev/null +++ b/src/state/is-within-visible-bounds-of-droppable.js @@ -0,0 +1,17 @@ +// @flow +import isWithin from './is-within'; +import type { + Position, + DroppableDimension, + DimensionFragment, +} from '../types'; + +export default (target: Position, droppable: DroppableDimension): boolean => { + const fragment: DimensionFragment = droppable.page.withMargin; + const { top, right, bottom, left } = fragment; + + console.log('is within droppable?', fragment, target); + + return isWithin(left, right)(target.x) && + isWithin(top, bottom)(target.y); +}; diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index d5936a843d..de002bc384 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -1,5 +1,5 @@ // @flow -import { add, distance } from '../position'; +import { add, distance, patch, subtract } from '../position'; import isWithin from '../is-within'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import type { @@ -33,11 +33,9 @@ export default ({ return null; } - const containerScroll: Position = destination.scroll.current; - const isWithinMainAxis = isWithin( - destination.page.withMargin[axis.start] + containerScroll[axis.line], - destination.page.withMargin[axis.end] + containerScroll[axis.line] + destination.page.withMargin[axis.start], + destination.page.withMargin[axis.end] ); const result: DraggableDimension[] = options diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index f95a835250..2591cd05dc 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -2,6 +2,7 @@ import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; import getClosestDraggable from './get-closest-draggable'; import moveToNewDroppable from './move-to-new-droppable/'; +import { add, subtract } from '../position'; import type { Result } from './move-to-new-droppable'; import type { DraggableId, @@ -45,6 +46,8 @@ export default ({ const draggable: DraggableDimension = draggables[draggableId]; const source: DroppableDimension = droppables[droppableId]; + // not considering the container scroll changes as container scrolling cancels a keyboard drag + const destination: ?DroppableDimension = getBestCrossAxisDroppable({ isMovingForward, pageCenter, @@ -57,15 +60,9 @@ export default ({ return null; } - // const newSiblings: DraggableDimension[] = getDraggablesInsideDroppable( - // destination, - // draggables, - // ); - const target: ?DraggableDimension = getClosestDraggable({ axis: destination.axis, pageCenter, - scrollOffset: destination.scroll.current, destination, draggables, }); diff --git a/src/state/reducer.js b/src/state/reducer.js index 1c675fe695..07ef1f3209 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -23,8 +23,9 @@ import type { TypeId, import { add, subtract, negate } from './position'; import getDragImpact from './get-drag-impact'; import moveToNextIndex from './move-to-next-index/'; +import isWithinVisibleBoundsOfDroppable from './is-within-visible-bounds-of-droppable'; import type { Result as MoveToNextResult } from './move-to-next-index/move-to-next-index-types'; -import type { Result as MoveToNewDroppable } from './move-to-best-droppable/move-to-new-droppable'; +import type { Result as MoveToNewDroppable } from './move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types'; import moveToBestDroppable from './move-to-best-droppable/'; const noDimensions: DimensionState = { @@ -298,6 +299,14 @@ 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.current.shouldAnimate) { + return clean(); + } + const { id, offset } = action.payload; const target: ?DroppableDimension = state.dimension.droppable[id]; @@ -438,11 +447,13 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } + const droppable: DroppableDimension = state.dimension.droppable[existing.impact.destination.droppableId]; + const result: ?MoveToNextResult = moveToNextIndex({ isMovingForward, draggableId: existing.current.id, impact: existing.impact, - droppable: state.dimension.droppable[existing.impact.destination.droppableId], + droppable, draggables: state.dimension.draggable, }); @@ -452,23 +463,19 @@ export default (state: State = clean('IDLE'), action: Action): State => { } const impact: DragImpact = result.impact; - - // const page: Position = add(existing.current.page.selection, diff); - // const client: Position = add(existing.current.client.selection, diff); + const page: Position = result.pageCenter; + const client: Position = subtract(page, existing.current.windowScroll); // current limitation: cannot go beyond visible border of list - // const droppableId: ?DroppableId = getDroppableOver( - // result.center, state.dimension.droppable, - // ); - - // if (!droppableId) { - // // eslint-disable-next-line no-console - // console.info('currently not supporting moving a draggable outside the visibility bounds of a droppable'); - // return state; - // } + const isVisible: boolean = isWithinVisibleBoundsOfDroppable( + page, droppable, + ); - const page: Position = result.pageCenter; - const client: Position = subtract(page, existing.current.windowScroll); + if (!isVisible) { + // eslint-disable-next-line no-console + console.info('currently not supporting moving a draggable outside the visibility bounds of a droppable'); + return state; + } return move({ state, diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 993aa122fa..a0ab56e196 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -7,7 +7,7 @@ import getWindowScrollPosition from '../get-window-scroll-position'; import { getDroppableDimension } from '../../state/dimension'; import getClosestScrollable from '../get-closest-scrollable'; // eslint-disable-next-line no-duplicate-imports -import type { Margin } from '../../state/dimension'; +import type { Margin, ClientRect } from '../../state/dimension'; import type { DroppableDimension, Position, HTMLElement } from '../../types'; import type { Props } from './droppable-dimension-publisher-types'; @@ -46,10 +46,41 @@ export default class DroppableDimensionPublisher extends Component { left: parseInt(style.marginLeft, 10), }; + const clientRect: ClientRect = (() => { + const current: ClientRect = targetRef.getBoundingClientRect(); + + if (!this.closestScrollable) { + return current; + } + + if (this.closestScrollable === targetRef) { + return current; + } + + // need to trim dimensions + const parent: ClientRect = this.closestScrollable.getBoundingClientRect(); + + const top = Math.max(current.top, parent.top); + const left = Math.max(current.left, parent.left); + const right = Math.min(current.right, parent.right); + const bottom = Math.min(current.bottom, parent.bottom); + + const result: ClientRect = { + top, + right, + bottom, + left, + width: (right - left), + height: (bottom - top), + }; + + return result; + })(); + const dimension: DroppableDimension = getDroppableDimension({ id: droppableId, direction, - clientRect: targetRef.getBoundingClientRect(), + clientRect, margin, windowScroll: getWindowScrollPosition(), scroll: this.getScrollOffset(), diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js index 142c60cb3f..fcf20f92c3 100644 --- a/stories/5-multiple-vertical-lists-story.js +++ b/stories/5-multiple-vertical-lists-story.js @@ -12,9 +12,9 @@ const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( ); const initialQuotes = { - alpha: namespaceQuoteIds(getQuotes(8), 'alpha'), + alpha: namespaceQuoteIds(getQuotes(20), 'alpha'), beta: namespaceQuoteIds(getQuotes(3), 'beta'), - gamma: namespaceQuoteIds(getQuotes(6), 'gamma'), + gamma: namespaceQuoteIds(getQuotes(20), 'gamma'), delta: namespaceQuoteIds(getQuotes(0), 'delta'), }; diff --git a/stories/src/multiple-vertical/quote-app.jsx b/stories/src/multiple-vertical/quote-app.jsx index 9b48763e58..57792d2caa 100644 --- a/stories/src/multiple-vertical/quote-app.jsx +++ b/stories/src/multiple-vertical/quote-app.jsx @@ -151,6 +151,7 @@ export default class QuoteApp extends Component { Date: Fri, 8 Sep 2017 21:38:43 +1000 Subject: [PATCH 074/117] progress --- .../is-within-visible-bounds-of-droppable.js | 2 - .../get-closest-draggable.js | 17 ++-- src/state/move-to-best-droppable/index.js | 18 +++- .../move-to-new-droppable/index.js | 11 +-- .../move-to-new-droppable/to-foreign-list.js | 1 - src/state/reducer.js | 4 +- .../get-closest-draggable.spec.js | 99 ++++--------------- 7 files changed, 47 insertions(+), 105 deletions(-) diff --git a/src/state/is-within-visible-bounds-of-droppable.js b/src/state/is-within-visible-bounds-of-droppable.js index 77d5c6108a..126f2b3f7f 100644 --- a/src/state/is-within-visible-bounds-of-droppable.js +++ b/src/state/is-within-visible-bounds-of-droppable.js @@ -10,8 +10,6 @@ export default (target: Position, droppable: DroppableDimension): boolean => { const fragment: DimensionFragment = droppable.page.withMargin; const { top, right, bottom, left } = fragment; - console.log('is within droppable?', fragment, target); - return isWithin(left, right)(target.x) && isWithin(top, bottom)(target.y); }; diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-to-best-droppable/get-closest-draggable.js index de002bc384..e8963129fc 100644 --- a/src/state/move-to-best-droppable/get-closest-draggable.js +++ b/src/state/move-to-best-droppable/get-closest-draggable.js @@ -1,12 +1,10 @@ // @flow -import { add, distance, patch, subtract } from '../position'; +import { distance } from '../position'; import isWithin from '../is-within'; -import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; import type { Axis, Position, DraggableDimension, - DraggableDimensionMap, DroppableDimension, } from '../../types'; @@ -15,21 +13,18 @@ type Args = {| pageCenter: Position, // the droppable that is being moved to destination: DroppableDimension, - draggables: DraggableDimensionMap, + // the droppables inside the destination + insideDestination: DraggableDimension[], |} export default ({ axis, pageCenter, destination, - draggables, + insideDestination, }: Args): ?DraggableDimension => { - const options: DraggableDimension[] = getDraggablesInsideDroppable( - destination, draggables - ); - // Empty list - bail out - if (!options.length) { + if (!insideDestination.length) { return null; } @@ -38,7 +33,7 @@ export default ({ destination.page.withMargin[axis.end] ); - const result: DraggableDimension[] = options + const result: DraggableDimension[] = insideDestination // Remove any options that are hidden by overflow // Whole draggable must be visible to move to it .filter((draggable: DraggableDimension) => diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-to-best-droppable/index.js index 2591cd05dc..93971ae0b6 100644 --- a/src/state/move-to-best-droppable/index.js +++ b/src/state/move-to-best-droppable/index.js @@ -2,8 +2,8 @@ import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; import getClosestDraggable from './get-closest-draggable'; import moveToNewDroppable from './move-to-new-droppable/'; -import { add, subtract } from '../position'; -import type { Result } from './move-to-new-droppable'; +import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; +import type { Result } from './move-to-new-droppable/move-to-new-droppable-types'; import type { DraggableId, DroppableId, @@ -60,20 +60,30 @@ export default ({ return null; } + const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable( + destination, draggables + ); + const target: ?DraggableDimension = getClosestDraggable({ axis: destination.axis, pageCenter, destination, - draggables, + insideDestination, }); + // Draggables available, but none are candidates for movement (eg none are visible) + // Cannot move into the list + if (insideDestination.length && !target) { + return null; + } + return moveToNewDroppable({ pageCenter, draggable, target, destination, + insideDestination, home, impact, - draggables, }); }; diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/index.js b/src/state/move-to-best-droppable/move-to-new-droppable/index.js index 7df13093f0..6a8e4198d1 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/index.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/index.js @@ -2,7 +2,6 @@ import toHomeList from './to-home-list'; import toForeignList from './to-foreign-list'; import { patch } from '../../position'; -import getDraggablesInsideDroppable from '../../get-draggables-inside-droppable'; import type { Result } from './move-to-new-droppable-types'; import type { Position, @@ -10,7 +9,6 @@ import type { DraggableDimension, DroppableDimension, DraggableLocation, - DraggableDimensionMap, } from '../../../types'; type Args = {| @@ -23,12 +21,12 @@ type Args = {| target: ?DraggableDimension, // the droppable the draggable is moving to destination: DroppableDimension, + // all the draggables inside the destination + insideDestination: DraggableDimension[], // the source location of the draggable home: DraggableLocation, // the current drag impact impact: DragImpact, - // all the draggables in the system - draggables: DraggableDimensionMap, |} export default ({ @@ -37,11 +35,8 @@ export default ({ draggable, target, home, - draggables, + insideDestination, }: Args): ?Result => { - const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable( - destination, draggables - ); const amount: Position = patch( destination.axis.line, draggable.page.withMargin[destination.axis.size] diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js b/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js index 9922902969..59295aa684 100644 --- a/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js +++ b/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js @@ -27,7 +27,6 @@ export default ({ draggable, droppable, }: Args): ?Result => { - console.log('to-foreign-list.js'); const axis: Axis = droppable.axis; // Moving to an empty list diff --git a/src/state/reducer.js b/src/state/reducer.js index 07ef1f3209..fd3ee857db 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -447,7 +447,9 @@ export default (state: State = clean('IDLE'), action: Action): State => { return clean(); } - const droppable: DroppableDimension = state.dimension.droppable[existing.impact.destination.droppableId]; + const droppable: DroppableDimension = state.dimension.droppable[ + existing.impact.destination.droppableId + ]; const result: ?MoveToNextResult = moveToNextIndex({ isMovingForward, diff --git a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js index 71ac4af7a7..2de2d245f6 100644 --- a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js +++ b/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js @@ -9,22 +9,8 @@ import type { Position, DraggableDimension, DroppableDimension, - DraggableDimensionMap, } from '../../../../src/types'; -const withScroll = (droppable: DroppableDimension, scroll: Position): DroppableDimension => { - // $ExpectError - using spread - const updated: DroppableDimension = { - ...droppable, - scroll: { - initial: droppable.scroll.initial, - current: scroll, - }, - }; - - return updated; -}; - describe('get closest draggable', () => { [vertical, horizontal].forEach((axis: Axis) => { const start: number = 0; @@ -100,13 +86,13 @@ describe('get closest draggable', () => { }), }); - const draggables: DraggableDimensionMap = { - [partialHiddenBackwards.id]: partialHiddenBackwards, - [visible1.id]: visible1, - [visible2.id]: visible2, - [partiallyHiddenForwards.id]: partiallyHiddenForwards, - [hidden.id]: hidden, - }; + const insideDestination: DraggableDimension[] = [ + partialHiddenBackwards, + visible1, + visible2, + partiallyHiddenForwards, + hidden, + ]; it('should return the closest draggable', () => { // closet to visible1 @@ -117,7 +103,7 @@ describe('get closest draggable', () => { axis, pageCenter: center1, destination: droppable, - draggables, + insideDestination, }); expect(result1).toBe(visible1); @@ -129,7 +115,7 @@ describe('get closest draggable', () => { axis, pageCenter: center2, destination: droppable, - draggables, + insideDestination, }); expect(result2).toBe(visible2); }); @@ -139,13 +125,12 @@ describe('get closest draggable', () => { x: 100, y: 100, }; - const empty: DraggableDimensionMap = {}; const result: ?DraggableDimension = getClosestDraggable({ axis, pageCenter: center, destination: droppable, - draggables: empty, + insideDestination: [], }); expect(result).toBe(null); @@ -163,7 +148,7 @@ describe('get closest draggable', () => { axis, pageCenter: center, destination: droppable, - draggables, + insideDestination, }); expect(result).toBe(visible1); @@ -178,7 +163,7 @@ describe('get closest draggable', () => { axis, pageCenter: center, destination: droppable, - draggables, + insideDestination, }); expect(result).toBe(visible2); @@ -193,18 +178,18 @@ describe('get closest draggable', () => { axis, pageCenter: center, destination: droppable, - draggables, + insideDestination, }); expect(result).toBe(visible2); }); it('should return null if there are no visible targets', () => { - const notVisible: DraggableDimensionMap = { - [partialHiddenBackwards.id]: partialHiddenBackwards, - [partiallyHiddenForwards.id]: partiallyHiddenForwards, - [hidden.id]: hidden, - }; + const notVisible: DraggableDimension[] = [ + partialHiddenBackwards, + partiallyHiddenForwards, + hidden, + ]; const center: Position = { x: 0, y: 0, @@ -214,53 +199,11 @@ describe('get closest draggable', () => { axis, pageCenter: center, destination: droppable, - draggables: notVisible, + insideDestination: notVisible, }); expect(result).toBe(null); }); - - describe('taking into account droppable scroll', () => { - it('should include forward items that otherwise would be excluded', () => { - const center: Position = patch( - axis.line, partiallyHiddenForwards.page.withoutMargin.center[axis.line], 100 - ); - // need to scroll so that partiallyHiddenForwards is now visible - const diffToEnd: number = - partiallyHiddenForwards.page.withoutMargin[axis.end] - - droppable.page.withoutMargin[axis.end]; - - const scroll: Position = patch(axis.line, diffToEnd); - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: withScroll(droppable, scroll), - draggables, - }); - - expect(result).toBe(partiallyHiddenForwards); - }); - - it.only('should include backward items that otherwise would be excluded', () => { - const center: Position = patch( - axis.line, partialHiddenBackwards.page.withoutMargin.center[axis.line], 100 - ); - // need to scroll so that partialHiddenBackwards is now visible - const diffToEnd: number = partialHiddenBackwards.page.withoutMargin[axis.start] - - droppable.page.withoutMargin[axis.start]; - const scroll: Position = patch(axis.line, diffToEnd); - - const result: ?DraggableDimension = getClosestDraggable({ - axis, - pageCenter: center, - destination: withScroll(droppable, scroll), - draggables, - }); - - expect(result).toBe(partialHiddenBackwards); - }); - }); }); it('should return the draggable that is first on the main axis in the event of a tie', () => { @@ -276,7 +219,7 @@ describe('get closest draggable', () => { axis, pageCenter: center, destination: droppable, - draggables, + insideDestination, }); expect(result).toBe(visible1); @@ -292,7 +235,7 @@ describe('get closest draggable', () => { axis, pageCenter: add(center, patch(axis.line, 1)), destination: droppable, - draggables, + insideDestination, }); expect(result2).toBe(visible2); }); From 6a7a1ccd9a9ca8784840d5b6bd7250edf62cd356 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 21:50:37 +1000 Subject: [PATCH 075/117] renaming move-to-best-droppable to move-cross-axis --- .../get-best-cross-axis-droppable.js | 0 .../get-closest-draggable.js | 0 .../{move-to-best-droppable => move-cross-axis}/index.js | 0 .../move-to-new-droppable/index.js | 0 .../move-to-new-droppable/move-to-new-droppable-types.js | 0 .../move-to-new-droppable/to-foreign-list.js | 0 .../move-to-new-droppable/to-home-list.js | 0 src/state/reducer.js | 7 ++++--- .../get-best-cross-axis-droppable.spec.js | 0 .../get-closest-draggable.spec.js | 0 .../state/move-cross-axis/move-to-best-droppable.spec.js | 2 ++ .../state/move-cross-axis/move-to-new-droppable.spec.js | 5 +++++ .../move-to-best-droppable/move-to-new-droppable.spec.js | 0 13 files changed, 11 insertions(+), 3 deletions(-) rename src/state/{move-to-best-droppable => move-cross-axis}/get-best-cross-axis-droppable.js (100%) rename src/state/{move-to-best-droppable => move-cross-axis}/get-closest-draggable.js (100%) rename src/state/{move-to-best-droppable => move-cross-axis}/index.js (100%) rename src/state/{move-to-best-droppable => move-cross-axis}/move-to-new-droppable/index.js (100%) rename src/state/{move-to-best-droppable => move-cross-axis}/move-to-new-droppable/move-to-new-droppable-types.js (100%) rename src/state/{move-to-best-droppable => move-cross-axis}/move-to-new-droppable/to-foreign-list.js (100%) rename src/state/{move-to-best-droppable => move-cross-axis}/move-to-new-droppable/to-home-list.js (100%) rename test/unit/state/{move-to-best-droppable => move-cross-axis}/get-best-cross-axis-droppable.spec.js (100%) rename test/unit/state/{move-to-best-droppable => move-cross-axis}/get-closest-draggable.spec.js (100%) create mode 100644 test/unit/state/move-cross-axis/move-to-best-droppable.spec.js create mode 100644 test/unit/state/move-cross-axis/move-to-new-droppable.spec.js delete mode 100644 test/unit/state/move-to-best-droppable/move-to-new-droppable.spec.js diff --git a/src/state/move-to-best-droppable/get-best-cross-axis-droppable.js b/src/state/move-cross-axis/get-best-cross-axis-droppable.js similarity index 100% rename from src/state/move-to-best-droppable/get-best-cross-axis-droppable.js rename to src/state/move-cross-axis/get-best-cross-axis-droppable.js diff --git a/src/state/move-to-best-droppable/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js similarity index 100% rename from src/state/move-to-best-droppable/get-closest-draggable.js rename to src/state/move-cross-axis/get-closest-draggable.js diff --git a/src/state/move-to-best-droppable/index.js b/src/state/move-cross-axis/index.js similarity index 100% rename from src/state/move-to-best-droppable/index.js rename to src/state/move-cross-axis/index.js diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/index.js b/src/state/move-cross-axis/move-to-new-droppable/index.js similarity index 100% rename from src/state/move-to-best-droppable/move-to-new-droppable/index.js rename to src/state/move-cross-axis/move-to-new-droppable/index.js diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js b/src/state/move-cross-axis/move-to-new-droppable/move-to-new-droppable-types.js similarity index 100% rename from src/state/move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types.js rename to src/state/move-cross-axis/move-to-new-droppable/move-to-new-droppable-types.js diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js similarity index 100% rename from src/state/move-to-best-droppable/move-to-new-droppable/to-foreign-list.js rename to src/state/move-cross-axis/move-to-new-droppable/to-foreign-list.js diff --git a/src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js b/src/state/move-cross-axis/move-to-new-droppable/to-home-list.js similarity index 100% rename from src/state/move-to-best-droppable/move-to-new-droppable/to-home-list.js rename to src/state/move-cross-axis/move-to-new-droppable/to-home-list.js diff --git a/src/state/reducer.js b/src/state/reducer.js index fd3ee857db..120afa922f 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -25,8 +25,8 @@ import getDragImpact from './get-drag-impact'; import moveToNextIndex from './move-to-next-index/'; import isWithinVisibleBoundsOfDroppable from './is-within-visible-bounds-of-droppable'; import type { Result as MoveToNextResult } from './move-to-next-index/move-to-next-index-types'; -import type { Result as MoveToNewDroppable } from './move-to-best-droppable/move-to-new-droppable/move-to-new-droppable-types'; -import moveToBestDroppable from './move-to-best-droppable/'; +import type { Result as MoveCrossAxisResult } from './move-cross-axis/move-to-new-droppable/move-to-new-droppable-types'; +import moveCrossAxis from './move-cross-axis/'; const noDimensions: DimensionState = { request: null, @@ -469,6 +469,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { const client: Position = subtract(page, existing.current.windowScroll); // current limitation: cannot go beyond visible border of list + // TODO: need to allow movement into the placeholder space const isVisible: boolean = isWithinVisibleBoundsOfDroppable( page, droppable, ); @@ -510,7 +511,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { const droppableId: DroppableId = state.drag.impact.destination.droppableId; const home: DraggableLocation = state.drag.initial.source; - const result: ?MoveToNewDroppable = moveToBestDroppable({ + const result: ?MoveCrossAxisResult = moveCrossAxis({ isMovingForward: action.type === 'CROSS_AXIS_MOVE_FORWARD', pageCenter: center, draggableId, diff --git a/test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js b/test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js similarity index 100% rename from test/unit/state/move-to-best-droppable/get-best-cross-axis-droppable.spec.js rename to test/unit/state/move-cross-axis/get-best-cross-axis-droppable.spec.js diff --git a/test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js b/test/unit/state/move-cross-axis/get-closest-draggable.spec.js similarity index 100% rename from test/unit/state/move-to-best-droppable/get-closest-draggable.spec.js rename to test/unit/state/move-cross-axis/get-closest-draggable.spec.js diff --git a/test/unit/state/move-cross-axis/move-to-best-droppable.spec.js b/test/unit/state/move-cross-axis/move-to-best-droppable.spec.js new file mode 100644 index 0000000000..ceaff2d3b9 --- /dev/null +++ b/test/unit/state/move-cross-axis/move-to-best-droppable.spec.js @@ -0,0 +1,2 @@ +// need to add tests for checks done in move-to-best-droppable +// eg returning null when no matches are found 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 new file mode 100644 index 0000000000..79219297cb --- /dev/null +++ b/test/unit/state/move-cross-axis/move-to-new-droppable.spec.js @@ -0,0 +1,5 @@ +// @flow + +describe('move to new droppable', () => { + +}); diff --git a/test/unit/state/move-to-best-droppable/move-to-new-droppable.spec.js b/test/unit/state/move-to-best-droppable/move-to-new-droppable.spec.js deleted file mode 100644 index e69de29bb2..0000000000 From 9389db58ee86b8dd30d17f0fd5d5b0b2ac7240dc Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 22:30:15 +1000 Subject: [PATCH 076/117] adding tests --- .../move-to-new-droppable/index.js | 5 +-- .../move-to-new-droppable/to-foreign-list.js | 8 ++--- .../move-to-new-droppable/to-home-list.js | 4 +-- .../move-to-new-droppable.spec.js | 31 +++++++++++++++++++ 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/state/move-cross-axis/move-to-new-droppable/index.js b/src/state/move-cross-axis/move-to-new-droppable/index.js index 6a8e4198d1..440a10eaf1 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/index.js +++ b/src/state/move-cross-axis/move-to-new-droppable/index.js @@ -42,9 +42,6 @@ export default ({ draggable.page.withMargin[destination.axis.size] ); - const isGoingBeforeTarget: boolean = Boolean(target && - pageCenter[destination.axis.line] < target.page.withMargin.center[destination.axis.line]); - // moving back to the home list if (destination.id === draggable.droppableId) { return toHomeList({ @@ -60,7 +57,7 @@ export default ({ // moving to a foreign list return toForeignList({ amount, - isGoingBeforeTarget, + pageCenter, target, insideDroppable: insideDestination, draggable, 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 59295aa684..c1086686c1 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 @@ -12,7 +12,7 @@ import type { type Args = {| amount: Position, - isGoingBeforeTarget: boolean, + pageCenter: Position, target: ?DraggableDimension, insideDroppable: DraggableDimension[], draggable: DraggableDimension, @@ -21,13 +21,15 @@ type Args = {| export default ({ amount, - isGoingBeforeTarget, + pageCenter, target, insideDroppable, draggable, droppable, }: Args): ?Result => { const axis: Axis = droppable.axis; + const isGoingBeforeTarget: boolean = Boolean(target && + pageCenter[droppable.axis.line] < target.page.withMargin.center[droppable.axis.line]); // Moving to an empty list @@ -88,8 +90,6 @@ export default ({ .slice(proposedIndex, insideDroppable.length) .map((dimension: DraggableDimension): DraggableId => dimension.id); - console.log('moved', needsToMove); - const newImpact: DragImpact = { movement: { draggables: needsToMove, 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 29ed7141de..e4bea5237b 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 @@ -13,7 +13,6 @@ import type { type Args = {| amount: Position, - isGoingBeforeTarget: boolean, originalIndex: number, target: ?DraggableDimension, insideDroppable: DraggableDimension[], @@ -22,14 +21,13 @@ type Args = {| |} export default ({ + amount, originalIndex, target, - amount, insideDroppable, draggable, droppable, }: Args): ?Result => { - console.log('to-home-list.js'); if (!target) { console.error('there will always be a target in the original list'); return null; 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 79219297cb..c301a34429 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,5 +1,36 @@ // @flow describe('move to new droppable', () => { + describe('to home list', () => { + it('should return null and log an error if no target is found', () => { + // this should never happen but just being safe + }); + + it('should return null and log an error if the target is not inside the droppable', () => { + + }); + + describe('moving back into original index', () => { + + }); + + describe('moving before the original index', () => { + + }); + + describe('moving after the original index', () => { + + }); + }); + + describe('to foreign list', () => { + describe('is going before target', () => { + + }); + + describe('is going after target', () => { + + }); + }); }); From e3fe56f5d14f77c4f6ed55fda4756b1384b8e715 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Fri, 8 Sep 2017 22:42:27 +1000 Subject: [PATCH 077/117] adding more test cases to fill in --- .../move-to-new-droppable/to-home-list.js | 10 +++++----- .../move-cross-axis/move-to-new-droppable.spec.js | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) 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 e4bea5237b..171de12834 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 @@ -64,21 +64,21 @@ export default ({ }; } - const isMovingBeyondHome = targetIndex > originalIndex; + const isMovingBeyondOriginalIndex = targetIndex > originalIndex; - const edge: Edge = isMovingBeyondHome ? 'end' : 'start'; + const edge: Edge = isMovingBeyondOriginalIndex ? 'end' : 'start'; const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, sourceEdge: edge, - destination: isMovingBeyondHome ? target.page.withoutMargin : target.page.withMargin, + destination: isMovingBeyondOriginalIndex ? target.page.withoutMargin : target.page.withMargin, destinationEdge: edge, destinationAxis: axis, }); const needsToMove: DraggableId[] = (() => { // TODO: explain the index trickery - if (isMovingBeyondHome) { + if (isMovingBeyondOriginalIndex) { // need to ensure that the list is sorted with the closest item being first return insideDroppable.slice(originalIndex + 1, targetIndex + 1).reverse(); } @@ -91,7 +91,7 @@ export default ({ draggables: needsToMove, amount, // TODO: not sure what this should be - isBeyondStartPosition: isMovingBeyondHome, + isBeyondStartPosition: isMovingBeyondOriginalIndex, }, direction: axis.direction, destination: { 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 c301a34429..fa4fed8850 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 @@ -12,7 +12,13 @@ describe('move to new droppable', () => { }); describe('moving back into original index', () => { + it('should return the original center', () => { + }); + + it('should return an empty impact with the original location', () => { + + }); }); describe('moving before the original index', () => { From 630d623dc780cb57dc3d502f345688d0460fecae Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Sun, 10 Sep 2017 19:45:40 +1000 Subject: [PATCH 078/117] adding page visiblity checks --- .../move-to-new-droppable/to-home-list.js | 3 +-- .../move-to-next-index/in-foreign-list.js | 20 ++++++++++++++++++- src/state/move-to-next-index/in-home-list.js | 11 ++++++++++ src/state/reducer.js | 13 ------------ .../move-to-new-droppable.spec.js | 10 ++++++++++ 5 files changed, 41 insertions(+), 16 deletions(-) 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 171de12834..49369adf32 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,6 @@ export default ({ } const isMovingBeyondOriginalIndex = targetIndex > originalIndex; - const edge: Edge = isMovingBeyondOriginalIndex ? 'end' : 'start'; const newCenter: Position = moveToEdge({ @@ -90,7 +89,7 @@ export default ({ movement: { draggables: needsToMove, amount, - // TODO: not sure what this should be + // TODO: not sure what this should be isBeyondStartPosition: isMovingBeyondOriginalIndex, }, direction: axis.direction, 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 b46686e93c..69ddc340b8 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -1,5 +1,6 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; +import isWithinVisibleBoundsOfDroppable from '../is-within-visible-bounds-of-droppable'; import { patch } from '../position'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; @@ -55,12 +56,13 @@ export default ({ Math.min(proposedIndex, lastIndex) ]; + const isMovingPastLastIndex: boolean = proposedIndex > lastIndex; const sourceEdge: Edge = 'start'; const destinationEdge: Edge = (() => { // moving past the last item // in this case we are moving relative to the last item // as there is nothing at the proposed index. - if (proposedIndex > lastIndex) { + if (isMovingPastLastIndex) { return 'end'; } @@ -75,6 +77,22 @@ 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; + } + + return isWithinVisibleBoundsOfDroppable( + newCenter, droppable, + ); + })(); + + if (!isVisible) { + return null; + } + // When we are in foreign list we are only displacing items forward // This list is always sorted by the closest impacted draggable const moved: DraggableId[] = isMovingForward ? 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 4a1a376cb1..ec7c9839e2 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,7 @@ // @flow import memoizeOne from 'memoize-one'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; +import isWithinVisibleBoundsOfDroppable from '../is-within-visible-bounds-of-droppable'; import { patch } from '../position'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; @@ -81,6 +82,16 @@ export default ({ destinationAxis: droppable.axis, }); + // Currently not supporting moving a draggable outside the visibility bounds of a droppable + + const isVisible: boolean = isWithinVisibleBoundsOfDroppable( + newCenter, droppable, + ); + + if (!isVisible) { + return null; + } + // Calculate DragImpact // List is sorted where the items closest to where the draggable is currently go first diff --git a/src/state/reducer.js b/src/state/reducer.js index 120afa922f..017255770d 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -23,7 +23,6 @@ import type { TypeId, import { add, subtract, negate } from './position'; import getDragImpact from './get-drag-impact'; import moveToNextIndex from './move-to-next-index/'; -import isWithinVisibleBoundsOfDroppable from './is-within-visible-bounds-of-droppable'; import type { Result as MoveToNextResult } from './move-to-next-index/move-to-next-index-types'; import type { Result as MoveCrossAxisResult } from './move-cross-axis/move-to-new-droppable/move-to-new-droppable-types'; import moveCrossAxis from './move-cross-axis/'; @@ -468,18 +467,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { const page: Position = result.pageCenter; const client: Position = subtract(page, existing.current.windowScroll); - // current limitation: cannot go beyond visible border of list - // TODO: need to allow movement into the placeholder space - const isVisible: boolean = isWithinVisibleBoundsOfDroppable( - page, droppable, - ); - - if (!isVisible) { - // eslint-disable-next-line no-console - console.info('currently not supporting moving a draggable outside the visibility bounds of a droppable'); - return state; - } - return move({ state, impact, 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 fa4fed8850..1059bbc083 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 @@ -22,11 +22,21 @@ describe('move to new droppable', () => { }); describe('moving before the original index', () => { + describe('center', () => { + }); + describe('impact', () => { + + }); }); describe('moving after the original index', () => { + describe('center', () => { + }); + describe('impact', () => { + + }); }); }); From 76c59d5023b11bcf29ae359a927a39ef3720abe7 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Sun, 10 Sep 2017 20:14:38 +1000 Subject: [PATCH 079/117] now finding all options within a droppable --- src/state/move-cross-axis/get-closest-draggable.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index e8963129fc..6c5d4b667a 100644 --- a/src/state/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-cross-axis/get-closest-draggable.js @@ -29,19 +29,18 @@ export default ({ } const isWithinMainAxis = isWithin( - destination.page.withMargin[axis.start], - destination.page.withMargin[axis.end] + destination.page.withoutMargin[axis.start], + destination.page.withoutMargin[axis.end] ); const result: DraggableDimension[] = insideDestination // Remove any options that are hidden by overflow // Whole draggable must be visible to move to it - .filter((draggable: DraggableDimension) => - isWithinMainAxis(draggable.page.withMargin[axis.start]) && - isWithinMainAxis(draggable.page.withMargin[axis.end]) - ) + .filter((draggable: DraggableDimension) => ( + isWithinMainAxis(draggable.page.withoutMargin[axis.start]) && + isWithinMainAxis(draggable.page.withoutMargin[axis.end]) + )) .sort((a: DraggableDimension, b: DraggableDimension): number => { - // TODO: not considering scroll offset const distanceToA = distance(pageCenter, a.page.withMargin.center); const distanceToB = distance(pageCenter, b.page.withMargin.center); From 0011793e0d2ae1a34b027af6763f75dd6483b5bb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 11 Sep 2017 11:13:56 +1000 Subject: [PATCH 080/117] correcting visiblity check for when inside a dimension --- src/state/get-droppable-over.js | 4 +- .../is-within-visible-bounds-of-droppable.js | 33 +++++++++++++--- .../move-cross-axis/get-closest-draggable.js | 38 ++++++++----------- .../move-to-next-index/in-foreign-list.js | 6 +-- src/state/move-to-next-index/in-home-list.js | 6 +-- .../get-closest-draggable.spec.js | 4 +- 6 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/state/get-droppable-over.js b/src/state/get-droppable-over.js index 03cb5ee206..73fcde7231 100644 --- a/src/state/get-droppable-over.js +++ b/src/state/get-droppable-over.js @@ -1,6 +1,6 @@ // @flow import { droppableMapToList } from './dimension-map-to-list'; -import isWithinVisibleBoundsOfDroppable from './is-within-visible-bounds-of-droppable'; +import { isPointWithin } from './is-within-visible-bounds-of-droppable'; import type { DroppableId, Position, @@ -14,7 +14,7 @@ export default ( ): ?DroppableId => { const maybe: ?DroppableDimension = droppableMapToList(droppables) .find((droppable: DroppableDimension): boolean => ( - isWithinVisibleBoundsOfDroppable(target, droppable) + isPointWithin(droppable)(target) )); return maybe ? maybe.id : null; diff --git a/src/state/is-within-visible-bounds-of-droppable.js b/src/state/is-within-visible-bounds-of-droppable.js index 126f2b3f7f..64bac99a05 100644 --- a/src/state/is-within-visible-bounds-of-droppable.js +++ b/src/state/is-within-visible-bounds-of-droppable.js @@ -2,14 +2,37 @@ import isWithin from './is-within'; import type { Position, + DraggableDimension, DroppableDimension, DimensionFragment, } from '../types'; -export default (target: Position, droppable: DroppableDimension): boolean => { - const fragment: DimensionFragment = droppable.page.withMargin; - const { top, right, bottom, left } = fragment; +export const isPointWithin = (droppable: DroppableDimension) => { + const { top, right, bottom, left } = droppable.page.withMargin; - return isWithin(left, right)(target.x) && - isWithin(top, bottom)(target.y); + const isWithinHorizontal = isWithin(left, right); + const isWithinVertical = isWithin(top, bottom); + + return (point: Position): boolean => ( + isWithinHorizontal(point.x) && + isWithinVertical(point.y) + ); +}; + +export const isDraggableWithin = (droppable: DroppableDimension) => { + const { top, right, bottom, left } = droppable.page.withMargin; + + // There are some extremely minor inaccuracy in the calculations of margins (~0.001) + // To allow for this inaccuracy we make the dimension slightly bigger for this calculation + const isWithinHorizontal = isWithin(left - 1, right + 1); + const isWithinVertical = isWithin(top - 1, bottom + 1); + + return (draggable: DraggableDimension): boolean => { + const fragment: DimensionFragment = draggable.page.withMargin; + + return isWithinHorizontal(fragment.left) && + isWithinHorizontal(fragment.right) && + isWithinVertical(fragment.top) && + isWithinVertical(fragment.bottom); + }; }; diff --git a/src/state/move-cross-axis/get-closest-draggable.js b/src/state/move-cross-axis/get-closest-draggable.js index 6c5d4b667a..bb53c44c54 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 { distance } from '../position'; -import isWithin from '../is-within'; +import { isDraggableWithin } from '../is-within-visible-bounds-of-droppable'; import type { Axis, Position, @@ -28,36 +28,30 @@ export default ({ return null; } - const isWithinMainAxis = isWithin( - destination.page.withoutMargin[axis.start], - destination.page.withoutMargin[axis.end] - ); + const isWithinDestination = isDraggableWithin(destination); const result: DraggableDimension[] = insideDestination - // Remove any options that are hidden by overflow - // Whole draggable must be visible to move to it - .filter((draggable: DraggableDimension) => ( - isWithinMainAxis(draggable.page.withoutMargin[axis.start]) && - isWithinMainAxis(draggable.page.withoutMargin[axis.end]) - )) - .sort((a: DraggableDimension, b: DraggableDimension): number => { - const distanceToA = distance(pageCenter, a.page.withMargin.center); - const distanceToB = distance(pageCenter, b.page.withMargin.center); + // Remove any options that are hidden by overflow + // Whole draggable must be visible to move to it + .filter(isWithinDestination) + .sort((a: DraggableDimension, b: DraggableDimension): number => { + const distanceToA = distance(pageCenter, a.page.withMargin.center); + const distanceToB = distance(pageCenter, b.page.withMargin.center); // if a is closer - return a - if (distanceToA < distanceToB) { - return -1; - } + if (distanceToA < distanceToB) { + return -1; + } // if b is closer - return b - if (distanceToB < distanceToA) { - return 1; - } + if (distanceToB < distanceToA) { + return 1; + } // if the distance to a and b are the same: // return the one that appears first on the main axis - return a.page.withMargin[axis.start] - b.page.withMargin[axis.start]; - }); + return a.page.withMargin[axis.start] - b.page.withMargin[axis.start]; + }); return result.length ? result[0] : null; }; 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 69ddc340b8..35f3d6e628 100644 --- a/src/state/move-to-next-index/in-foreign-list.js +++ b/src/state/move-to-next-index/in-foreign-list.js @@ -1,6 +1,6 @@ // @flow import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import isWithinVisibleBoundsOfDroppable from '../is-within-visible-bounds-of-droppable'; +import { isPointWithin } from '../is-within-visible-bounds-of-droppable'; import { patch } from '../position'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; @@ -84,9 +84,7 @@ export default ({ return true; } - return isWithinVisibleBoundsOfDroppable( - newCenter, droppable, - ); + return isPointWithin(droppable)(newCenter); })(); if (!isVisible) { 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 ec7c9839e2..e33e68b9e1 100644 --- a/src/state/move-to-next-index/in-home-list.js +++ b/src/state/move-to-next-index/in-home-list.js @@ -1,7 +1,7 @@ // @flow import memoizeOne from 'memoize-one'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import isWithinVisibleBoundsOfDroppable from '../is-within-visible-bounds-of-droppable'; +import { isPointWithin } from '../is-within-visible-bounds-of-droppable'; import { patch } from '../position'; import moveToEdge from '../move-to-edge'; import type { Edge } from '../move-to-edge'; @@ -84,9 +84,7 @@ export default ({ // Currently not supporting moving a draggable outside the visibility bounds of a droppable - const isVisible: boolean = isWithinVisibleBoundsOfDroppable( - newCenter, droppable, - ); + const isVisible: boolean = isPointWithin(droppable)(newCenter); if (!isVisible) { return null; 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 2de2d245f6..0ac6e23772 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,5 +1,5 @@ // @flow -import getClosestDraggable from '../../../../src/state/move-to-best-droppable/get-closest-draggable'; +import getClosestDraggable from '../../../../src/state/move-cross-axis/get-closest-draggable'; import { getDroppableDimension, getDraggableDimension } from '../../../../src/state/dimension'; import { add, distance, patch } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; @@ -224,7 +224,7 @@ describe('get closest draggable', () => { expect(result).toBe(visible1); - // validating test assumptions + // validating test assumptions // 1. that they have equal distances expect(distance(center, visible1.page.withoutMargin.center)) From 0058fdc65680c2bbb4ce491e97d275882c5a9fac Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 11 Sep 2017 11:40:15 +1000 Subject: [PATCH 081/117] disallowing container scroll while drag is occuring --- src/state/action-creators.js | 7 ++++++- src/state/reducer.js | 2 +- src/types.js | 2 ++ src/view/draggable/draggable.jsx | 9 +++++++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 38d7304d5c..ad12719c4b 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -72,6 +72,7 @@ export type CompleteLiftAction = {| client: InitialDragLocation, page: InitialDragLocation, windowScroll: Position, + allowScroll: boolean, |} |} @@ -80,6 +81,7 @@ const completeLift = (id: DraggableId, client: InitialDragLocation, page: InitialDragLocation, windowScroll: Position, + allowScroll: boolean, ): CompleteLiftAction => ({ type: 'COMPLETE_LIFT', payload: { @@ -88,6 +90,7 @@ const completeLift = (id: DraggableId, client, page, windowScroll, + allowScroll, }, }); @@ -428,6 +431,7 @@ export type LiftAction = {| client: InitialDragLocation, page: InitialDragLocation, windowScroll: Position, + allowScroll: boolean, |} |} @@ -437,6 +441,7 @@ export const lift = (id: DraggableId, client: InitialDragLocation, page: InitialDragLocation, windowScroll: Position, + allowScroll: boolean, ) => (dispatch: Dispatch, getState: Function) => { (() => { const state: State = getState(); @@ -474,7 +479,7 @@ export const lift = (id: DraggableId, if (newState.phase !== 'COLLECTING_DIMENSIONS') { return; } - dispatch(completeLift(id, type, client, page, windowScroll)); + dispatch(completeLift(id, type, client, page, windowScroll, allowScroll)); }); }); }; diff --git a/src/state/reducer.js b/src/state/reducer.js index 017255770d..e9af85301d 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -302,7 +302,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { // 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.current.shouldAnimate) { + if (!state.drag.current.allowScroll) { return clean(); } diff --git a/src/types.js b/src/types.js index eaf0e3fafa..afc50232b2 100644 --- a/src/types.js +++ b/src/types.js @@ -144,6 +144,8 @@ export type CurrentDragLocation = {| export type CurrentDrag = {| id: DraggableId, type: TypeId, + // whether scrolling is allowed - otherwise a scroll will cancel the drag + allowScroll: boolean, // viewport client: CurrentDragLocation, // viewport + scroll diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 24f8a37caf..154602e458 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -121,7 +121,10 @@ export default class Draggable extends Component { center: add(client.center, windowScroll), }; - lift(draggableId, type, client, page, windowScroll); + // Allowing scrolling with a mouse when lifting with a mouse + const allowScroll = true; + + lift(draggableId, type, client, page, windowScroll, allowScroll); } onKeyLift = () => { @@ -142,8 +145,10 @@ export default class Draggable extends Component { selection: add(center, windowScroll), center: add(center, windowScroll), }; + // not allowing scrolling with a mouse when lifting with a keyboard + const allowScroll = false; - lift(draggableId, type, client, page, windowScroll); + lift(draggableId, type, client, page, windowScroll, allowScroll); } onMove = (client: Position) => { From 630564bb753b26769985f5b97156d1999b052440 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 11 Sep 2017 14:43:37 +1000 Subject: [PATCH 082/117] renaming allowScroll to isScrollAllowed --- src/state/action-creators.js | 12 ++++++------ src/state/reducer.js | 2 +- src/types.js | 2 +- src/view/draggable/draggable.jsx | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index ad12719c4b..b69ebcfe09 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -72,7 +72,7 @@ export type CompleteLiftAction = {| client: InitialDragLocation, page: InitialDragLocation, windowScroll: Position, - allowScroll: boolean, + isScrollAllowed: boolean, |} |} @@ -81,7 +81,7 @@ const completeLift = (id: DraggableId, client: InitialDragLocation, page: InitialDragLocation, windowScroll: Position, - allowScroll: boolean, + isScrollAllowed: boolean, ): CompleteLiftAction => ({ type: 'COMPLETE_LIFT', payload: { @@ -90,7 +90,7 @@ const completeLift = (id: DraggableId, client, page, windowScroll, - allowScroll, + isScrollAllowed, }, }); @@ -431,7 +431,7 @@ export type LiftAction = {| client: InitialDragLocation, page: InitialDragLocation, windowScroll: Position, - allowScroll: boolean, + isScrollAllowed: boolean, |} |} @@ -441,7 +441,7 @@ export const lift = (id: DraggableId, client: InitialDragLocation, page: InitialDragLocation, windowScroll: Position, - allowScroll: boolean, + isScrollAllowed: boolean, ) => (dispatch: Dispatch, getState: Function) => { (() => { const state: State = getState(); @@ -479,7 +479,7 @@ export const lift = (id: DraggableId, if (newState.phase !== 'COLLECTING_DIMENSIONS') { return; } - dispatch(completeLift(id, type, client, page, windowScroll, allowScroll)); + dispatch(completeLift(id, type, client, page, windowScroll, isScrollAllowed)); }); }); }; diff --git a/src/state/reducer.js b/src/state/reducer.js index e9af85301d..08370db745 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -302,7 +302,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { // 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.current.allowScroll) { + if (!state.drag.current.isScrollAllowed) { return clean(); } diff --git a/src/types.js b/src/types.js index afc50232b2..34c965cfa9 100644 --- a/src/types.js +++ b/src/types.js @@ -145,7 +145,7 @@ export type CurrentDrag = {| id: DraggableId, type: TypeId, // whether scrolling is allowed - otherwise a scroll will cancel the drag - allowScroll: boolean, + isScrollAllowed: boolean, // viewport client: CurrentDragLocation, // viewport + scroll diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 154602e458..0551ac2e41 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -122,9 +122,9 @@ export default class Draggable extends Component { }; // Allowing scrolling with a mouse when lifting with a mouse - const allowScroll = true; + const isScrollAllowed = true; - lift(draggableId, type, client, page, windowScroll, allowScroll); + lift(draggableId, type, client, page, windowScroll, isScrollAllowed); } onKeyLift = () => { @@ -146,9 +146,9 @@ export default class Draggable extends Component { center: add(center, windowScroll), }; // not allowing scrolling with a mouse when lifting with a keyboard - const allowScroll = false; + const isScrollAllowed = false; - lift(draggableId, type, client, page, windowScroll, allowScroll); + lift(draggableId, type, client, page, windowScroll, isScrollAllowed); } onMove = (client: Position) => { From 88b5b39e4039a4dba418f87684d06021e2f9efbc Mon Sep 17 00:00:00 2001 From: Jared Crowe Date: Mon, 11 Sep 2017 15:27:43 +1000 Subject: [PATCH 083/117] add tests for dragging between lists to get-drag-impact.spec (#82) --- test/unit/state/get-drag-impact.spec.js | 211 ++++++++++++++++++ .../state/get-new-home-client-offset.spec.js | 68 +----- test/utils/create-droppable.js | 69 ++++++ 3 files changed, 281 insertions(+), 67 deletions(-) create mode 100644 test/utils/create-droppable.js diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index f5b6badee1..ac633a0935 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -7,6 +7,8 @@ import { import getDragImpact from '../../../src/state/get-drag-impact'; import noImpact from '../../../src/state/no-impact'; import getClientRect from '../../utils/get-client-rect'; +import createDroppable from '../../utils/create-droppable'; +import { add, patch } from '../../../src/state/position'; import type { WithinDroppable, DroppableId, @@ -1133,4 +1135,213 @@ describe('get drag impact', () => { }); }); }); + + describe('moving between lists', () => { + const homeDroppable = createDroppable({ + direction: 'vertical', + droppableId: 'drop-home', + droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, + draggableRects: [ + { top: 0, left: 0, bottom: 100, right: 100 }, + { top: 101, left: 0, bottom: 300, right: 100 }, + { top: 301, left: 0, bottom: 600, right: 100 }, + ], + }); + + const destinationDroppable = createDroppable({ + droppableId: 'drop-destination', + droppableRect: { top: 100, left: 110, bottom: 800, right: 210 }, + draggableRects: [ + { top: 100, left: 110, bottom: 400, right: 210 }, + { top: 401, left: 110, bottom: 600, right: 210 }, + { top: 601, left: 110, bottom: 700, right: 210 }, + ], + }); + + const droppables = { + [homeDroppable.droppableId]: homeDroppable.droppable, + [destinationDroppable.droppableId]: destinationDroppable.droppable, + }; + + const draggables = { + ...homeDroppable.draggables, + ...destinationDroppable.draggables, + }; + + const draggableId = homeDroppable.draggableIds[0]; + const draggedItem = homeDroppable.draggables[draggableId]; + + describe('moving outside a droppable', () => { + const page = { + x: homeDroppable.droppable.page.withMargin.center.x, + y: homeDroppable.droppable.page.withMargin.height + 1, + }; + const withinDroppable = { center: page }; + const impact = getDragImpact({ + page, + withinDroppable, + draggableId, + draggables, + droppables, + }); + + it('should not return a destination', () => { + expect(impact.destination).toBe(null); + }); + it('should not return a movement amount', () => { + expect(impact.movement.amount).toEqual(origin); + }); + it('should not displace any items', () => { + expect(impact.movement.draggables.length).toBe(0); + }); + }); + + describe('moving to the start of a foreign droppable', () => { + const page = { + x: destinationDroppable.droppable.page.withMargin.center.x, + y: destinationDroppable.droppable.page.withMargin.top + 1, + }; + const withinDroppable = { center: page }; + const impact = getDragImpact({ + page, + withinDroppable, + draggableId, + draggables, + droppables, + }); + + it('should return the destination droppable', () => { + expect(impact.destination && impact.destination.droppableId) + .toBe(destinationDroppable.droppableId); + }); + it('should return an index of 0 (first position)', () => { + expect(impact.destination && impact.destination.index).toEqual(0); + }); + it('should indicate that items must be displaced forwards', () => { + expect(impact.movement.isBeyondStartPosition).toBe(false); + }); + it('should indicate that items need to be displaced by the height of the dragged item', () => { + const expected = patch('y', draggedItem.page.withMargin.height); + expect(impact.movement.amount).toEqual(expected); + }); + it('should displace all items in the destination droppable', () => { + expect(impact.movement.draggables).toEqual(destinationDroppable.draggableIds); + }); + }); + + describe('moving to the second position of a foreign droppable', () => { + const page = { + x: destinationDroppable.droppable.page.withMargin.center.x, + y: destinationDroppable.draggables[ + destinationDroppable.draggableIds[1] + ].page.withMargin.top + 1, + }; + const withinDroppable = { center: page }; + const impact = getDragImpact({ + page, + withinDroppable, + draggableId, + draggables, + droppables, + }); + + it('should return the destination droppable', () => { + expect(impact.destination && impact.destination.droppableId) + .toBe(destinationDroppable.droppableId); + }); + it('should return an index of 1 (second position)', () => { + expect(impact.destination && impact.destination.index).toEqual(1); + }); + it('should indicate that items must be displaced forwards', () => { + expect(impact.movement.isBeyondStartPosition).toBe(false); + }); + it('should indicate that items need to be displaced by the height of the dragged item', () => { + const expected = patch('y', draggedItem.page.withMargin.height); + expect(impact.movement.amount).toEqual(expected); + }); + it('should displace all items in the destination droppable except the first', () => { + expect(impact.movement.draggables).toEqual( + destinationDroppable.draggableIds.slice(1 - destinationDroppable.draggableIds.length) + ); + }); + }); + + describe('moving to the end of a foreign droppable', () => { + const page = { + x: destinationDroppable.droppable.page.withMargin.center.x, + y: destinationDroppable.droppable.page.withMargin.bottom - 1, + }; + const withinDroppable = { center: page }; + const impact = getDragImpact({ + page, + withinDroppable, + draggableId, + draggables, + droppables, + }); + + it('should return the destination droppable', () => { + expect(impact.destination && impact.destination.droppableId) + .toBe(destinationDroppable.droppableId); + }); + it('should return an index equal to the number of draggables in the destination droppable', () => { + expect(impact.destination && impact.destination.index) + .toEqual(destinationDroppable.draggableIds.length); + }); + it('should indicate that items must be displaced forwards', () => { + expect(impact.movement.isBeyondStartPosition).toBe(false); + }); + it('should indicate that items need to be displaced by the height of the dragged item', () => { + const expected = patch('y', draggedItem.page.withMargin.height); + expect(impact.movement.amount).toEqual(expected); + }); + it('should not displace any items', () => { + expect(impact.movement.draggables.length).toBe(0); + }); + }); + + describe('when the foreign droppable is scrolled', () => { + // top of the first item + const page = { + x: destinationDroppable.droppable.page.withMargin.center.x, + y: destinationDroppable.droppable.page.withMargin.top + 1, + }; + + // scroll past the first item + const center = add(page, { + x: 0, + y: destinationDroppable.draggables[ + destinationDroppable.draggableIds[0] + ].page.withMargin.height, + }); + const withinDroppable = { center }; + const impact = getDragImpact({ + page, + withinDroppable, + draggableId, + draggables, + droppables, + }); + + it('should return the destination droppable', () => { + expect(impact.destination && impact.destination.droppableId) + .toBe(destinationDroppable.droppableId); + }); + it('should account for scrolling when calculating the index', () => { + expect(impact.destination && impact.destination.index).toEqual(1); + }); + it('should indicate that items must be displaced forwards', () => { + expect(impact.movement.isBeyondStartPosition).toBe(false); + }); + it('should indicate that items need to be displaced by the height of the dragged item', () => { + const expected = patch('y', draggedItem.page.withMargin.height); + expect(impact.movement.amount).toEqual(expected); + }); + it('should account for scrolling when determining which items are being displaced', () => { + expect(impact.movement.draggables).toEqual( + destinationDroppable.draggableIds.slice(1 - destinationDroppable.draggableIds.length) + ); + }); + }); + }); }); diff --git a/test/unit/state/get-new-home-client-offset.spec.js b/test/unit/state/get-new-home-client-offset.spec.js index 4a588332d4..d042bd4bfc 100644 --- a/test/unit/state/get-new-home-client-offset.spec.js +++ b/test/unit/state/get-new-home-client-offset.spec.js @@ -1,81 +1,15 @@ // @flow import getNewHomeClientOffset from '../../../src/state/get-new-home-client-offset'; import noImpact from '../../../src/state/no-impact'; -import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; import { add, negate, subtract } from '../../../src/state/position'; -import getClientRect from '../../utils/get-client-rect'; +import createDroppable from '../../utils/create-droppable'; import type { - DroppableId, DragMovement, Position, - DraggableDimension, - DraggableDimensionMap, - DroppableDimension, } from '../../../src/types'; -type Rect = {| - top: number, - left: number, - bottom: number, - right: number, -|}; - -type CreateDroppableArgs = {| - direction?: 'vertical' | 'horizontal', - droppableId: DroppableId, - droppableRect: Rect, - draggableRects: Rect[], -|}; - -type TestDroppable = { - droppableId: string, - droppable: DroppableDimension, - draggables: DraggableDimensionMap, - draggableIds: string[], - draggableDimensions: DraggableDimension[], -}; - const origin: Position = { x: 0, y: 0 }; -const createDroppable = ({ - direction = 'vertical', - droppableId, - droppableRect, - draggableRects, -}: CreateDroppableArgs): TestDroppable => { - const droppable = getDroppableDimension({ - id: droppableId, - direction, - clientRect: getClientRect(droppableRect), - }); - - const draggableDimensions = draggableRects.map( - (draggableRect, index) => getDraggableDimension({ - id: `${droppableId}::drag-${index}`, - droppableId, - clientRect: getClientRect(draggableRect), - }) - ); - - const draggables = draggableDimensions.reduce( - (currentDraggables, draggable) => ({ - ...currentDraggables, - [draggable.id]: draggable, - }), - {} - ); - - const draggableIds = Object.keys(draggables); - - return { - droppableId, - droppable, - draggables, - draggableIds, - draggableDimensions, - }; -}; - const getDistanceOverDraggables = dimension => arr => ({ [dimension === 'height' ? 'y' : 'x']: arr.reduce( (total, draggable) => total + draggable.page.withMargin[dimension] + 1, diff --git a/test/utils/create-droppable.js b/test/utils/create-droppable.js new file mode 100644 index 0000000000..56f29dfd0b --- /dev/null +++ b/test/utils/create-droppable.js @@ -0,0 +1,69 @@ +import { getDraggableDimension, getDroppableDimension } from '../../src/state/dimension'; +import type { + DroppableId, + DraggableDimension, + DraggableDimensionMap, + DroppableDimension, +} from '../../src/types'; +import getClientRect from './get-client-rect'; + +type Rect = {| + top: number, + left: number, + bottom: number, + right: number, +|}; + +type CreateDroppableArgs = {| + direction?: 'vertical' | 'horizontal', + droppableId: DroppableId, + droppableRect: Rect, + draggableRects: Rect[], +|}; + +type TestDroppable = { + droppableId: string, + droppable: DroppableDimension, + draggables: DraggableDimensionMap, + draggableIds: string[], + draggableDimensions: DraggableDimension[], +}; + +export default ({ + direction = 'vertical', + droppableId, + droppableRect, + draggableRects, +}: CreateDroppableArgs): TestDroppable => { + const droppable = getDroppableDimension({ + id: droppableId, + direction, + clientRect: getClientRect(droppableRect), + }); + + const draggableDimensions = draggableRects.map( + (draggableRect, index) => getDraggableDimension({ + id: `${droppableId}::drag-${index}`, + droppableId, + clientRect: getClientRect(draggableRect), + }) + ); + + const draggables = draggableDimensions.reduce( + (currentDraggables, draggable) => ({ + ...currentDraggables, + [draggable.id]: draggable, + }), + {} + ); + + const draggableIds = Object.keys(draggables); + + return { + droppableId, + droppable, + draggables, + draggableIds, + draggableDimensions, + }; +}; From 25a54017a03291b3136ccaef69d8d24abb02323a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 11 Sep 2017 15:30:07 +1000 Subject: [PATCH 084/117] adding some tests --- .../get-draggables-inside-droppable.spec.js | 2 - ...within-visible-bounds-of-droppable.spec.js | 96 +++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 test/unit/state/is-within-visible-bounds-of-droppable.spec.js diff --git a/test/unit/state/get-draggables-inside-droppable.spec.js b/test/unit/state/get-draggables-inside-droppable.spec.js index f4b95d1101..ecc9e60d06 100644 --- a/test/unit/state/get-draggables-inside-droppable.spec.js +++ b/test/unit/state/get-draggables-inside-droppable.spec.js @@ -94,6 +94,4 @@ describe('get draggables inside a droppable', () => { expect(result).toEqual([inside1, inside2, inside3]); }); - - // other edge cases tested in get-inside-dimension }); diff --git a/test/unit/state/is-within-visible-bounds-of-droppable.spec.js b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js new file mode 100644 index 0000000000..cfddb3866d --- /dev/null +++ b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js @@ -0,0 +1,96 @@ +// @flow +import { isPointWithin } from '../../../src/state/is-within-visible-bounds-of-droppable'; +import { getDroppableDimension } from '../../../src/state/dimension'; +import { add, subtract } from '../../../src/state/position'; +import getClientRect from '../../utils/get-client-rect'; +import type { + Position, +} from '../../../src/types'; + +describe('is within visible bounds of a droppable', () => { + const droppable = getDroppableDimension({ + id: 'droppable', + clientRect: getClientRect({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + }); + + describe('is point within', () => { + const isWithinDroppable = isPointWithin(droppable); + const { top, left, right, bottom } = droppable.page.withMargin; + + it('should return true if a point is within a droppable', () => { + expect(isWithinDroppable(droppable.page.withMargin.center)).toBe(true); + }); + + it('should return true if a point is on any of the droppable boundaries', () => { + const corners = [ + { x: left, y: top }, + { x: left, y: bottom }, + { x: right, y: top }, + { x: right, y: bottom }, + ]; + + corners.forEach((corner: Position) => { + expect(isWithinDroppable(corner)).toBe(true); + }); + }); + + it('should return false if the point is not within the droppable on any side', () => { + const outside = [ + subtract({ x: left, y: top }, { x: 0, y: 10 }), // too far top + subtract({ x: left, y: bottom }, { x: 10, y: 0 }), // too far left + add({ x: right, y: top }, { x: 10, y: 0 }), // too far right + add({ x: right, y: bottom }, { x: 0, y: 10 }), // too far bottom + ]; + + outside.forEach((point: Position) => { + expect(isWithinDroppable(point)).toBe(false); + }); + }); + + it('should be based on the page coordinates of the droppable', () => { + const windowScroll: Position = { + x: 200, y: 200, + }; + const custom = getDroppableDimension({ + id: 'with-scroll', + windowScroll, + clientRect: getClientRect({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), + }); + const isWithinCustom = isPointWithin(custom); + + // custom points + expect(isWithinCustom({ x: 10, y: 10 })).toBe(false); + expect(isWithinCustom({ x: 210, y: 210 })).toBe(true); + + // checking with the center position of the dimension itself + expect(isWithinCustom(custom.client.withMargin.center)).toBe(false); + expect(isWithinCustom(custom.page.withMargin.center)).toBe(true); + }); + }); + + describe('is draggable within', () => { + it('should return true if the draggable is within the droppable', () => { + const draggable = getDraggableDimension({ + + }); + }); + + it('should return false if there is overlap on any side', () => { + + }); + + it('should allow a small affordance to compensate for margin capturing inaccuracy', () => { + + }); + }); +}); From 942307e22e9a7f10694c608481aba9b40f10eb82 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 11 Sep 2017 15:38:29 +1000 Subject: [PATCH 085/117] renaming create-droppable --- test/unit/state/get-drag-impact.spec.js | 6 +++--- .../state/get-new-home-client-offset.spec.js | 12 ++++++------ ...within-visible-bounds-of-droppable.spec.js | 14 +++++++++++--- ...le.js => get-droppable-with-draggables.js} | 19 +++++++++---------- 4 files changed, 29 insertions(+), 22 deletions(-) rename test/utils/{create-droppable.js => get-droppable-with-draggables.js} (79%) diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index ac633a0935..eebc7cee02 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -7,7 +7,7 @@ import { import getDragImpact from '../../../src/state/get-drag-impact'; import noImpact from '../../../src/state/no-impact'; import getClientRect from '../../utils/get-client-rect'; -import createDroppable from '../../utils/create-droppable'; +import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; import { add, patch } from '../../../src/state/position'; import type { WithinDroppable, @@ -1137,7 +1137,7 @@ describe('get drag impact', () => { }); describe('moving between lists', () => { - const homeDroppable = createDroppable({ + const homeDroppable = getDroppableWithDraggables({ direction: 'vertical', droppableId: 'drop-home', droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, @@ -1148,7 +1148,7 @@ describe('get drag impact', () => { ], }); - const destinationDroppable = createDroppable({ + const destinationDroppable = getDroppableWithDraggables({ droppableId: 'drop-destination', droppableRect: { top: 100, left: 110, bottom: 800, right: 210 }, draggableRects: [ diff --git a/test/unit/state/get-new-home-client-offset.spec.js b/test/unit/state/get-new-home-client-offset.spec.js index d042bd4bfc..67dff75812 100644 --- a/test/unit/state/get-new-home-client-offset.spec.js +++ b/test/unit/state/get-new-home-client-offset.spec.js @@ -2,7 +2,7 @@ import getNewHomeClientOffset from '../../../src/state/get-new-home-client-offset'; import noImpact from '../../../src/state/no-impact'; import { add, negate, subtract } from '../../../src/state/position'; -import createDroppable from '../../utils/create-droppable'; +import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; import type { DragMovement, Position, @@ -22,7 +22,7 @@ const getHorizontalDistanceOverDraggables = getDistanceOverDraggables('width'); describe('get new home client offset', () => { describe('vertical', () => { - const droppable = createDroppable({ + const droppable = getDroppableWithDraggables({ droppableId: 'drop-1', droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, draggableRects: [ @@ -356,7 +356,7 @@ describe('get new home client offset', () => { }); describe('horizontal', () => { - const droppable = createDroppable({ + const droppable = getDroppableWithDraggables({ direction: 'horizontal', droppableId: 'drop-1', droppableRect: { top: 0, left: 0, bottom: 100, right: 500 }, @@ -692,7 +692,7 @@ describe('get new home client offset', () => { }); describe('multiple lists - vertical', () => { - const homeDroppable = createDroppable({ + const homeDroppable = getDroppableWithDraggables({ droppableId: 'drop-home', droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, draggableRects: [ @@ -702,7 +702,7 @@ describe('get new home client offset', () => { ], }); - const destinationDroppable = createDroppable({ + const destinationDroppable = getDroppableWithDraggables({ droppableId: 'drop-destination', droppableRect: { top: 100, left: 110, bottom: 700, right: 210 }, draggableRects: [ @@ -712,7 +712,7 @@ describe('get new home client offset', () => { ], }); - const emptyDroppable = createDroppable({ + const emptyDroppable = getDroppableWithDraggables({ droppableId: 'drop-empty', droppableRect: { top: 200, left: 220, bottom: 800, right: 320 }, draggableRects: [], diff --git a/test/unit/state/is-within-visible-bounds-of-droppable.spec.js b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js index cfddb3866d..57ef6383a8 100644 --- a/test/unit/state/is-within-visible-bounds-of-droppable.spec.js +++ b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js @@ -1,8 +1,9 @@ // @flow -import { isPointWithin } from '../../../src/state/is-within-visible-bounds-of-droppable'; +import { isPointWithin, isDraggableWithin } from '../../../src/state/is-within-visible-bounds-of-droppable'; import { getDroppableDimension } from '../../../src/state/dimension'; import { add, subtract } from '../../../src/state/position'; import getClientRect from '../../utils/get-client-rect'; +import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; import type { Position, } from '../../../src/types'; @@ -80,9 +81,16 @@ describe('is within visible bounds of a droppable', () => { describe('is draggable within', () => { it('should return true if the draggable is within the droppable', () => { - const draggable = getDraggableDimension({ - + const result = getDroppableWithDraggables({ + direction: 'vertical', + droppableRect: { top: 0, left: 0, bottom: 100, right: 100 }, + draggableRects: [ + { top: 0, left: 0, bottom: 20, right: 100 }, + ], }); + const isWithinDroppable = isDraggableWithin(result.droppable); + + expect(isWithinDroppable(result.draggables[0])).toBe(true); }); it('should return false if there is overlap on any side', () => { diff --git a/test/utils/create-droppable.js b/test/utils/get-droppable-with-draggables.js similarity index 79% rename from test/utils/create-droppable.js rename to test/utils/get-droppable-with-draggables.js index 56f29dfd0b..a495b7275d 100644 --- a/test/utils/create-droppable.js +++ b/test/utils/get-droppable-with-draggables.js @@ -4,6 +4,7 @@ import type { DraggableDimension, DraggableDimensionMap, DroppableDimension, + Direction, } from '../../src/types'; import getClientRect from './get-client-rect'; @@ -14,14 +15,14 @@ type Rect = {| right: number, |}; -type CreateDroppableArgs = {| - direction?: 'vertical' | 'horizontal', +type Args = {| + direction?: Direction, droppableId: DroppableId, droppableRect: Rect, draggableRects: Rect[], |}; -type TestDroppable = { +type Result = { droppableId: string, droppable: DroppableDimension, draggables: DraggableDimensionMap, @@ -34,14 +35,14 @@ export default ({ droppableId, droppableRect, draggableRects, -}: CreateDroppableArgs): TestDroppable => { - const droppable = getDroppableDimension({ +}: Args): Result => { + const droppable: DroppableDimension = getDroppableDimension({ id: droppableId, direction, clientRect: getClientRect(droppableRect), }); - const draggableDimensions = draggableRects.map( + const draggableDimensions: DraggableDimension[] = draggableRects.map( (draggableRect, index) => getDraggableDimension({ id: `${droppableId}::drag-${index}`, droppableId, @@ -49,13 +50,11 @@ export default ({ }) ); - const draggables = draggableDimensions.reduce( + const draggables: DraggableDimensionMap = draggableDimensions.reduce( (currentDraggables, draggable) => ({ ...currentDraggables, [draggable.id]: draggable, - }), - {} - ); + }), {}); const draggableIds = Object.keys(draggables); From 937c068357ee2c3c9e36074d75c2fef510d08ca8 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 11 Sep 2017 15:39:35 +1000 Subject: [PATCH 086/117] cleaning up types in test util --- test/utils/get-droppable-with-draggables.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/utils/get-droppable-with-draggables.js b/test/utils/get-droppable-with-draggables.js index a495b7275d..35c6261251 100644 --- a/test/utils/get-droppable-with-draggables.js +++ b/test/utils/get-droppable-with-draggables.js @@ -1,4 +1,5 @@ import { getDraggableDimension, getDroppableDimension } from '../../src/state/dimension'; +import type { ClientRect } from '../../src/state/dimension'; import type { DroppableId, DraggableDimension, @@ -8,18 +9,11 @@ import type { } from '../../src/types'; import getClientRect from './get-client-rect'; -type Rect = {| - top: number, - left: number, - bottom: number, - right: number, -|}; - type Args = {| direction?: Direction, droppableId: DroppableId, - droppableRect: Rect, - draggableRects: Rect[], + droppableRect: ClientRect, + draggableRects: ClientRect[], |}; type Result = { From 372195b156cbad09f12538cf0f713590ce84735a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Mon, 11 Sep 2017 15:51:12 +1000 Subject: [PATCH 087/117] tests for is-within-visible-bounds-of-droppable --- ...within-visible-bounds-of-droppable.spec.js | 36 ++++++++++++++++--- test/utils/get-droppable-with-draggables.js | 11 ++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/test/unit/state/is-within-visible-bounds-of-droppable.spec.js b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js index 57ef6383a8..4739f3bd7b 100644 --- a/test/unit/state/is-within-visible-bounds-of-droppable.spec.js +++ b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js @@ -4,8 +4,10 @@ import { getDroppableDimension } from '../../../src/state/dimension'; import { add, subtract } from '../../../src/state/position'; import getClientRect from '../../utils/get-client-rect'; import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; +import type { Result } from '../../utils/get-droppable-with-draggables'; import type { Position, + DraggableDimension, } from '../../../src/types'; describe('is within visible bounds of a droppable', () => { @@ -81,24 +83,50 @@ describe('is within visible bounds of a droppable', () => { describe('is draggable within', () => { it('should return true if the draggable is within the droppable', () => { - const result = getDroppableWithDraggables({ - direction: 'vertical', + const result: Result = getDroppableWithDraggables({ droppableRect: { top: 0, left: 0, bottom: 100, right: 100 }, draggableRects: [ - { top: 0, left: 0, bottom: 20, right: 100 }, + // on the boundaries + { top: 0, left: 0, bottom: 100, right: 100 }, ], }); const isWithinDroppable = isDraggableWithin(result.droppable); - expect(isWithinDroppable(result.draggables[0])).toBe(true); + expect(isWithinDroppable(result.draggableDimensions[0])).toBe(true); }); it('should return false if there is overlap on any side', () => { + const result: Result = getDroppableWithDraggables({ + droppableRect: { top: 0, left: 0, bottom: 100, right: 100 }, + draggableRects: [ + { top: -10, left: 0, bottom: 20, right: 100 }, // too far top + { top: 0, left: -10, bottom: 20, right: 100 }, // too far left + { top: 0, left: 0, bottom: 20, right: 110 }, // too far right + { top: 0, left: 0, bottom: 120, right: 100 }, // too far bottom + ], + }); + const isWithinDroppable = isDraggableWithin(result.droppable); + result.draggableDimensions.forEach((draggable: DraggableDimension) => { + expect(isWithinDroppable(draggable)).toBe(false); + }); }); it('should allow a small affordance to compensate for margin capturing inaccuracy', () => { + const result: Result = getDroppableWithDraggables({ + droppableRect: { top: 0, left: 0, bottom: 100, right: 100 }, + draggableRects: [ + { top: -1, left: 0, bottom: 20, right: 100 }, // not too far top + { top: 0, left: -1, bottom: 20, right: 100 }, // not too far left + { top: 0, left: 0, bottom: 20, right: 101 }, // not too far right + { top: 0, left: 0, bottom: 101, right: 100 }, // not too far bottom + ], + }); + const isWithinDroppable = isDraggableWithin(result.droppable); + result.draggableDimensions.forEach((draggable: DraggableDimension) => { + expect(isWithinDroppable(draggable)).toBe(true); + }); }); }); }); diff --git a/test/utils/get-droppable-with-draggables.js b/test/utils/get-droppable-with-draggables.js index 35c6261251..c2aba81424 100644 --- a/test/utils/get-droppable-with-draggables.js +++ b/test/utils/get-droppable-with-draggables.js @@ -9,14 +9,21 @@ import type { } from '../../src/types'; import getClientRect from './get-client-rect'; +type ClientRectSubset = { + top: number, + left: number, + right: number, + bottom: number, +} + type Args = {| direction?: Direction, droppableId: DroppableId, droppableRect: ClientRect, - draggableRects: ClientRect[], + draggableRects: ClientRectSubset[], |}; -type Result = { +export type Result = { droppableId: string, droppable: DroppableDimension, draggables: DraggableDimensionMap, From 6070b1896b4077db82edce9cac06c9e7a264664a Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 07:58:18 +1000 Subject: [PATCH 088/117] tests for move-to-new-droppable --- src/state/move-cross-axis/index.js | 2 +- ...able-types.js => move-cross-axis-types.js} | 3 +- .../move-to-new-droppable/index.js | 4 +- .../move-to-new-droppable/to-foreign-list.js | 10 +- .../move-to-new-droppable/to-home-list.js | 39 +- .../move-to-new-droppable.spec.js | 424 +++++++++++++++++- 6 files changed, 453 insertions(+), 29 deletions(-) rename src/state/move-cross-axis/{move-to-new-droppable/move-to-new-droppable-types.js => move-cross-axis-types.js} (71%) diff --git a/src/state/move-cross-axis/index.js b/src/state/move-cross-axis/index.js index 93971ae0b6..c7674762ed 100644 --- a/src/state/move-cross-axis/index.js +++ b/src/state/move-cross-axis/index.js @@ -3,7 +3,7 @@ import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; import getClosestDraggable from './get-closest-draggable'; import moveToNewDroppable from './move-to-new-droppable/'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import type { Result } from './move-to-new-droppable/move-to-new-droppable-types'; +import type { Result } from './move-cross-axis-types'; import type { DraggableId, DroppableId, diff --git a/src/state/move-cross-axis/move-to-new-droppable/move-to-new-droppable-types.js b/src/state/move-cross-axis/move-cross-axis-types.js similarity index 71% rename from src/state/move-cross-axis/move-to-new-droppable/move-to-new-droppable-types.js rename to src/state/move-cross-axis/move-cross-axis-types.js index c7f0f0d95c..e9957e9bb5 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/move-to-new-droppable-types.js +++ b/src/state/move-cross-axis/move-cross-axis-types.js @@ -1,5 +1,4 @@ -// @flow -import type { Position, DragImpact } from '../../../types'; +import type { Position, DragImpact } from '../../types'; export type Result = {| // how far the draggable needs to move to be in its new home diff --git a/src/state/move-cross-axis/move-to-new-droppable/index.js b/src/state/move-cross-axis/move-to-new-droppable/index.js index 440a10eaf1..a06f417493 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/index.js +++ b/src/state/move-cross-axis/move-to-new-droppable/index.js @@ -2,7 +2,7 @@ import toHomeList from './to-home-list'; import toForeignList from './to-foreign-list'; import { patch } from '../../position'; -import type { Result } from './move-to-new-droppable-types'; +import type { Result } from '../move-cross-axis-types'; import type { Position, DragImpact, @@ -25,8 +25,6 @@ type Args = {| insideDestination: DraggableDimension[], // the source location of the draggable home: DraggableLocation, - // the current drag impact - impact: DragImpact, |} export default ({ 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 c1086686c1..54aa07ba89 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 @@ -1,6 +1,6 @@ // @flow import moveToEdge from '../../move-to-edge'; -import type { Result } from './move-to-new-droppable-types'; +import type { Result } from '../move-cross-axis-types'; import type { Axis, Position, @@ -73,10 +73,6 @@ export default ({ console.error('could not find target inside destination'); return null; } - if (droppable.id === draggable.droppableId) { - console.error('to-foreign-list handles movement to foreign lists and not home lists'); - return null; - } const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, @@ -86,6 +82,10 @@ export default ({ destinationAxis: axis, }); + // Can only displace forward when moving into a foreign list + // if going before: move everything down including the target + // if going after: move everything down excluding the target + const needsToMove: DraggableId[] = insideDroppable .slice(proposedIndex, insideDroppable.length) .map((dimension: DraggableDimension): DraggableId => dimension.id); 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 49369adf32..421895058e 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,7 +1,7 @@ // @flow import moveToEdge from '../../move-to-edge'; import type { Edge } from '../../move-to-edge'; -import type { Result } from './move-to-new-droppable-types'; +import type { Result } from '../move-cross-axis-types'; import type { Axis, Position, @@ -64,33 +64,48 @@ export default ({ }; } - const isMovingBeyondOriginalIndex = targetIndex > originalIndex; - const edge: Edge = isMovingBeyondOriginalIndex ? 'end' : 'start'; + // When moving *before* where the item started: + // We align the dragging item top of the target + // and move everything from the target to the original position forwards + + // When moving *after* where the item started: + // We align the dragging item to the end of the target + // and move everything from the target to the original position backwards + + const isMovingPastOriginalIndex = targetIndex > originalIndex; + const edge: Edge = isMovingPastOriginalIndex ? 'end' : 'start'; const newCenter: Position = moveToEdge({ source: draggable.page.withoutMargin, sourceEdge: edge, - destination: isMovingBeyondOriginalIndex ? target.page.withoutMargin : target.page.withMargin, + destination: isMovingPastOriginalIndex ? target.page.withoutMargin : target.page.withMargin, destinationEdge: edge, destinationAxis: axis, }); const needsToMove: DraggableId[] = (() => { - // TODO: explain the index trickery - if (isMovingBeyondOriginalIndex) { - // need to ensure that the list is sorted with the closest item being first - return insideDroppable.slice(originalIndex + 1, targetIndex + 1).reverse(); + if (!isMovingPastOriginalIndex) { + return insideDroppable.slice(targetIndex, originalIndex); } - return insideDroppable.slice(targetIndex, originalIndex); - })() - .map((d: DraggableDimension): DraggableId => d.id); + + // We are aligning to the bottom of the target and moving everything + // back to the original index backwards + + // We want everything after the original index to move + const from: number = originalIndex + 1; + // We need the target to move backwards + const to: number = targetIndex + 1; + + // Need to ensure that the list is sorted with the closest item being first + return insideDroppable.slice(from, to).reverse(); + })().map((d: DraggableDimension): DraggableId => d.id); const newImpact: DragImpact = { movement: { draggables: needsToMove, amount, // TODO: not sure what this should be - isBeyondStartPosition: isMovingBeyondOriginalIndex, + isBeyondStartPosition: isMovingPastOriginalIndex, }, direction: axis.direction, destination: { 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 1059bbc083..7de69abc23 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,52 +1,464 @@ // @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 getClientRect from '../../../utils/get-client-rect'; +import moveToEdge from '../../../../src/state/move-to-edge'; +import { patch } from '../../../../src/state/position'; +import type { + Axis, + DragImpact, + DraggableDimension, + DroppableDimension, + Position, +} from '../../../../src/types'; describe('move to new droppable', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { }); + }); + + afterEach(() => { + console.error.mockRestore(); + }); + + const margin = { top: 0, left: 0, bottom: 10, right: 0 }; + + const home: DroppableDimension = getDroppableDimension({ + id: 'home', + clientRect: getClientRect({ top: 0, left: 0, right: 100, bottom: 200 }), + }); + // size: 10 + const inHome1: DraggableDimension = getDraggableDimension({ + id: 'inhome1', + droppableId: home.id, + margin, + clientRect: getClientRect({ top: 0, left: 0, right: 100, bottom: 10 }), + }); + // size: 20 + const inHome2: DraggableDimension = getDraggableDimension({ + id: 'inhome2', + droppableId: home.id, + // pushed down by margin of inHome1 + margin, + clientRect: getClientRect({ top: 20, left: 0, right: 100, bottom: 50 }), + }); + // size: 30 + const inHome3: DraggableDimension = getDraggableDimension({ + id: 'inhome3', + droppableId: home.id, + margin, + // pushed down by margin of inHome2 + clientRect: getClientRect({ top: 60, left: 0, right: 100, bottom: 90 }), + }); + // size: 40 + const inHome4: DraggableDimension = getDraggableDimension({ + id: 'inhome4', + droppableId: home.id, + // pushed down by margin of inHome3 + margin, + clientRect: getClientRect({ top: 100, left: 0, right: 100, bottom: 140 }), + }); + + // TODO: get working with horizonital axis describe('to home list', () => { + const dontCare: Position = { x: 0, y: 0 }; + const axis: Axis = home.axis; + const draggables: DraggableDimension[] = [inHome1, inHome2, inHome3, inHome4]; + it('should return null and log an error if no target is found', () => { // this should never happen but just being safe + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: inHome1, + target: null, + destination: home, + insideDestination: draggables, + home: { + index: 0, + droppableId: home.id, + }, + }); + expect(result).toBe(null); + expect(console.error).toHaveBeenCalled(); }); it('should return null and log an error if the target is not inside the droppable', () => { + const invalid: DraggableDimension = getDraggableDimension({ + id: 'invalid', + droppableId: 'some-other-droppable', + clientRect: getClientRect({ + top: 1000, + left: 1000, + bottom: 1100, + right: 1100, + }), + }); + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: draggables[0], + target: invalid, + destination: home, + insideDestination: draggables, + home: { + index: 0, + droppableId: home.id, + }, + }); + expect(result).toBe(null); + expect(console.error).toHaveBeenCalled(); }); describe('moving back into original index', () => { - it('should return the original center', () => { + // 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.id, + }, + }); + 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 an empty impact with the original location', () => { + const expected: DragImpact = { + movement: { + draggables: [], + amount: patch(axis.line, inHome2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: home.id, + index: 1, + }, + }; + expect(result.impact).toEqual(expected); }); }); describe('moving before the original index', () => { - describe('center', () => { + // 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.id, + }, + }); + + 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, + }); + + expect(result.pageCenter).toEqual(expected); }); - describe('impact', () => { + it('should move the everything from the target index to the original index forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + draggables: [inHome2.id, inHome3.id], + amount: patch(axis.line, inHome4.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: home.id, + // original index of target + index: 1, + }, + }; + + expect(result.impact).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.id, + }, + }); + + 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, + sourceEdge: 'end', + destination: inHome4.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + expect(result.pageCenter).toEqual(expected); + }); }); - describe('impact', () => { + it('should move the everything from the target index to the original index forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + draggables: [inHome4.id, inHome3.id, inHome2.id], + amount: patch(axis.line, inHome1.page.withMargin[axis.size]), + // is moving beyond start position + isBeyondStartPosition: true, + }, + direction: axis.direction, + destination: { + droppableId: home.id, + // original index of target + index: 3, + }, + }; + + expect(result.impact).toEqual(expected); }); }); }); describe('to foreign list', () => { - describe('is going before target', () => { + const foreign: DroppableDimension = getDroppableDimension({ + id: 'foreign', + clientRect: getClientRect({ top: 0, left: 100, right: 200, bottom: 200 }), + }); + // size: 10 + const inForeign1: DraggableDimension = getDraggableDimension({ + id: 'inForeign1', + droppableId: foreign.id, + margin, + clientRect: getClientRect({ top: 0, left: 0, right: 100, bottom: 10 }), + }); + // size: 20 + const inForeign2: DraggableDimension = getDraggableDimension({ + id: 'inForeign2', + droppableId: foreign.id, + // pushed down by margin of inForeign1 + margin, + clientRect: getClientRect({ top: 20, left: 0, right: 100, bottom: 50 }), + }); + // size: 30 + const inForeign3: DraggableDimension = getDraggableDimension({ + id: 'inForeign3', + droppableId: foreign.id, + margin, + // pushed down by margin of inForeign2 + clientRect: getClientRect({ top: 60, left: 0, right: 100, bottom: 90 }), + }); + // size: 40 + const inForeign4: DraggableDimension = getDraggableDimension({ + id: 'inForeign4', + droppableId: foreign.id, + margin, + // pushed down by margin of inForeign3 + clientRect: getClientRect({ top: 100, left: 0, right: 100, bottom: 140 }), + }); + const draggables: DraggableDimension[] = [ + inForeign1, inForeign2, inForeign3, inForeign4, + ]; + + it('should return null when the target is not within the list - cannot really happen', () => { + const result: ?Result = moveToNewDroppable({ + pageCenter: inHome1.page.withMargin.center, + draggable: inHome1, + target: inHome2, + destination: foreign, + insideDestination: draggables, + home: { + index: 0, + droppableId: home.id, + }, + }); + + expect(result).toBe(null); + }); + + 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.id, + }, + }); + + if (!result) { + throw new Error('invalid test setup'); + } + + it('should move to the start edge of the droppable', () => { + const expected: Position = moveToEdge({ + source: inHome1.page.withMargin, + sourceEdge: 'start', + destination: foreign.page.withMargin, + destinationEdge: 'start', + destinationAxis: foreign.axis, + }); + + expect(result.pageCenter).toEqual(expected); + }); + + it('should return an empty impact', () => { + const expected: DragImpact = { + movement: { + draggables: [], + amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), + isBeyondStartPosition: false, + }, + direction: foreign.axis.direction, + destination: { + droppableId: foreign.id, + index: 0, + }, + }; + + expect(result.impact).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.id, + }, + }); + + 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, + }); + + expect(result.pageCenter).toEqual(expected); + }); + + it('should move the target and everything below it forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + draggables: [inForeign2.id, inForeign3.id, inForeign4.id], + amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), + isBeyondStartPosition: false, + }, + direction: foreign.axis.direction, + destination: { + droppableId: foreign.id, + // index of foreign2 + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - describe('is going after target', () => { + 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.id, + }, + }); + + if (!result) { + throw new Error('invalid test setup'); + } + it('should move under 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); + }); + + it('should move everything after the proposed index forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + draggables: [inForeign3.id, inForeign4.id], + amount: patch(foreign.axis.line, inHome4.page.withMargin[foreign.axis.size]), + isBeyondStartPosition: false, + }, + direction: foreign.axis.direction, + destination: { + droppableId: foreign.id, + // going after target, so index is target index + 1 + index: 2, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); }); }); From 932de9fadf571212828ac70162ebfffacd2a4fac Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 08:23:05 +1000 Subject: [PATCH 089/117] adding horizontal tests for move-to-new-droppable --- .../move-to-new-droppable.spec.js | 856 ++++++++++-------- 1 file changed, 463 insertions(+), 393 deletions(-) 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 7de69abc23..43492d1212 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 @@ -5,6 +5,8 @@ import { getDraggableDimension, getDroppableDimension } from '../../../../src/st import getClientRect from '../../../utils/get-client-rect'; import moveToEdge from '../../../../src/state/move-to-edge'; import { patch } from '../../../../src/state/position'; +import { horizontal, vertical } from '../../../../src/state/axis'; +import type { Margin } from '../../../../src/state/dimension'; import type { Axis, DragImpact, @@ -22,442 +24,510 @@ describe('move to new droppable', () => { console.error.mockRestore(); }); - const margin = { top: 0, left: 0, bottom: 10, right: 0 }; + const noMargin: Margin = { top: 0, left: 0, bottom: 0, right: 0 }; - const home: DroppableDimension = getDroppableDimension({ - id: 'home', - clientRect: getClientRect({ top: 0, left: 0, right: 100, bottom: 200 }), - }); - // size: 10 - const inHome1: DraggableDimension = getDraggableDimension({ - id: 'inhome1', - droppableId: home.id, - margin, - clientRect: getClientRect({ top: 0, left: 0, right: 100, bottom: 10 }), - }); - // size: 20 - const inHome2: DraggableDimension = getDraggableDimension({ - id: 'inhome2', - droppableId: home.id, - // pushed down by margin of inHome1 - margin, - clientRect: getClientRect({ top: 20, left: 0, right: 100, bottom: 50 }), - }); - // size: 30 - const inHome3: DraggableDimension = getDraggableDimension({ - id: 'inhome3', - droppableId: home.id, - margin, - // pushed down by margin of inHome2 - clientRect: getClientRect({ top: 60, left: 0, right: 100, bottom: 90 }), - }); - // size: 40 - const inHome4: DraggableDimension = getDraggableDimension({ - id: 'inhome4', - droppableId: home.id, - // pushed down by margin of inHome3 - margin, - clientRect: getClientRect({ top: 100, left: 0, right: 100, bottom: 140 }), - }); + [vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const margin = { + ...noMargin, + [axis.end]: 10, + }; - // TODO: get working with horizonital axis - describe('to home list', () => { - const dontCare: Position = { x: 0, y: 0 }; - const axis: Axis = home.axis; - const draggables: DraggableDimension[] = [inHome1, inHome2, inHome3, inHome4]; - - it('should return null and log an error if no target is found', () => { - // this should never happen but just being safe - const result: ?Result = moveToNewDroppable({ - pageCenter: dontCare, - draggable: inHome1, - target: null, - destination: home, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.id, - }, - }); + const crossAxisStart: number = 0; + const crossAxisEnd: number = 100; - expect(result).toBe(null); - expect(console.error).toHaveBeenCalled(); - }); - - it('should return null and log an error if the target is not inside the droppable', () => { - const invalid: DraggableDimension = getDraggableDimension({ - id: 'invalid', - droppableId: 'some-other-droppable', + const home: DroppableDimension = getDroppableDimension({ + id: 'home', + direction: axis.direction, + clientRect: getClientRect({ + [axis.start]: 0, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.end]: 200, + }), + }); + // size: 10 + const inHome1: DraggableDimension = getDraggableDimension({ + id: 'inhome1', + droppableId: home.id, + margin, clientRect: getClientRect({ - top: 1000, - left: 1000, - bottom: 1100, - right: 1100, + [axis.start]: 0, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.end]: 10, }), }); - const result: ?Result = moveToNewDroppable({ - pageCenter: dontCare, - draggable: draggables[0], - target: invalid, - destination: home, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.id, - }, + // size: 20 + const inHome2: DraggableDimension = getDraggableDimension({ + id: 'inhome2', + droppableId: home.id, + // pushed forward by margin of inHome1 + margin, + clientRect: getClientRect({ + [axis.start]: 20, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.end]: 50, + }), + }); + // size: 30 + const inHome3: DraggableDimension = getDraggableDimension({ + id: 'inhome3', + droppableId: home.id, + margin, + // pushed forward by margin of inHome2 + clientRect: getClientRect({ + [axis.start]: 60, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.end]: 90, + }), + }); + // size: 40 + const inHome4: DraggableDimension = getDraggableDimension({ + id: 'inhome4', + droppableId: home.id, + // pushed forward by margin of inHome3 + margin, + clientRect: getClientRect({ + [axis.start]: 100, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.end]: 140, + }), }); - expect(result).toBe(null); - expect(console.error).toHaveBeenCalled(); - }); + // TODO: get working with horizonital axis + describe('to home list', () => { + const dontCare: Position = { x: 0, y: 0 }; + const draggables: DraggableDimension[] = [ + inHome1, inHome2, inHome3, inHome4, + ]; + + it('should return null and log an error if no target is found', () => { + // this should never happen but just being safe + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: inHome1, + target: null, + destination: home, + insideDestination: draggables, + home: { + index: 0, + droppableId: home.id, + }, + }); - 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.id, - }, - }); + expect(result).toBe(null); + expect(console.error).toHaveBeenCalled(); + }); - if (!result) { - throw new Error('invalid test setup'); - } + it('should return null and log an error if the target is not inside the droppable', () => { + const invalid: DraggableDimension = getDraggableDimension({ + id: 'invalid', + droppableId: 'some-other-droppable', + clientRect: getClientRect({ + top: 1000, + left: 1000, + bottom: 1100, + right: 1100, + }), + }); + const result: ?Result = moveToNewDroppable({ + pageCenter: dontCare, + draggable: draggables[0], + target: invalid, + destination: home, + insideDestination: draggables, + home: { + index: 0, + droppableId: home.id, + }, + }); - 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); - }); + expect(result).toBe(null); + expect(console.error).toHaveBeenCalled(); + }); - it('should return an empty impact with the original location', () => { - const expected: DragImpact = { - movement: { - draggables: [], - amount: patch(axis.line, inHome2.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: home.id, - index: 1, - }, - }; + 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.id, + }, + }); - expect(result.impact).toEqual(expected); - }); - }); + if (!result) { + throw new Error('invalid test setup'); + } - 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.id, - }, - }); + 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); + }); - 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 return an empty impact with the original location', () => { + const expected: DragImpact = { + movement: { + draggables: [], + amount: patch(axis.line, inHome2.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: home.id, + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - expect(result.pageCenter).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.id, + }, + }); - it('should move the everything from the target index to the original index forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - draggables: [inHome2.id, inHome3.id], - amount: patch(axis.line, inHome4.page.withMargin[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: home.id, - // original index of target - index: 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); + if (!result) { + throw new Error('invalid test setup'); + } - 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.id, - }, - }); + 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, + }); - 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, - sourceEdge: 'end', - destination: inHome4.page.withoutMargin, - destinationEdge: 'end', - 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 + draggables: [inHome2.id, inHome3.id], + amount: patch(axis.line, inHome4.page.withMargin[axis.size]), + isBeyondStartPosition: false, + }, + direction: axis.direction, + destination: { + droppableId: home.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 - draggables: [inHome4.id, inHome3.id, inHome2.id], - amount: patch(axis.line, inHome1.page.withMargin[axis.size]), - // is moving beyond start position - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { - droppableId: home.id, - // original index of target - index: 3, - }, - }; - - expect(result.impact).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.id, + }, + }); - describe('to foreign list', () => { - const foreign: DroppableDimension = getDroppableDimension({ - id: 'foreign', - clientRect: getClientRect({ top: 0, left: 100, right: 200, bottom: 200 }), - }); - // size: 10 - const inForeign1: DraggableDimension = getDraggableDimension({ - id: 'inForeign1', - droppableId: foreign.id, - margin, - clientRect: getClientRect({ top: 0, left: 0, right: 100, bottom: 10 }), - }); - // size: 20 - const inForeign2: DraggableDimension = getDraggableDimension({ - id: 'inForeign2', - droppableId: foreign.id, - // pushed down by margin of inForeign1 - margin, - clientRect: getClientRect({ top: 20, left: 0, right: 100, bottom: 50 }), - }); - // size: 30 - const inForeign3: DraggableDimension = getDraggableDimension({ - id: 'inForeign3', - droppableId: foreign.id, - margin, - // pushed down by margin of inForeign2 - clientRect: getClientRect({ top: 60, left: 0, right: 100, bottom: 90 }), - }); - // size: 40 - const inForeign4: DraggableDimension = getDraggableDimension({ - id: 'inForeign4', - droppableId: foreign.id, - margin, - // pushed down by margin of inForeign3 - clientRect: getClientRect({ top: 100, left: 0, right: 100, bottom: 140 }), - }); + 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, + sourceEdge: 'end', + destination: inHome4.page.withoutMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + + expect(result.pageCenter).toEqual(expected); + }); + }); - const draggables: DraggableDimension[] = [ - inForeign1, inForeign2, inForeign3, inForeign4, - ]; - - it('should return null when the target is not within the list - cannot really happen', () => { - const result: ?Result = moveToNewDroppable({ - pageCenter: inHome1.page.withMargin.center, - draggable: inHome1, - target: inHome2, - destination: foreign, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.id, - }, + it('should move the everything from the target index to the original index forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + draggables: [inHome4.id, inHome3.id, inHome2.id], + amount: patch(axis.line, inHome1.page.withMargin[axis.size]), + // is moving beyond start position + isBeyondStartPosition: true, + }, + direction: axis.direction, + destination: { + droppableId: home.id, + // original index of target + index: 3, + }, + }; + + expect(result.impact).toEqual(expected); + }); + }); }); - expect(result).toBe(null); - }); + describe('to foreign list', () => { + const foreignCrossAxisStart: number = 100; + const foreignCrossAxisEnd: number = 200; - 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.id, - }, - }); + const foreign: DroppableDimension = getDroppableDimension({ + id: 'foreign', + direction: axis.direction, + clientRect: getClientRect({ + [axis.start]: 0, + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.end]: 200, + }), + }); + // size: 10 + const inForeign1: DraggableDimension = getDraggableDimension({ + id: 'inForeign1', + droppableId: foreign.id, + margin, + clientRect: getClientRect({ + [axis.start]: 0, + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.end]: 10, + }), + }); + // size: 20 + const inForeign2: DraggableDimension = getDraggableDimension({ + id: 'inForeign2', + droppableId: foreign.id, + // pushed forward by margin of inForeign1 + margin, + clientRect: getClientRect({ + [axis.start]: 20, + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.end]: 50, + }), + }); + // size: 30 + const inForeign3: DraggableDimension = getDraggableDimension({ + id: 'inForeign3', + droppableId: foreign.id, + margin, + // pushed forward by margin of inForeign2 + clientRect: getClientRect({ + [axis.start]: 60, + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.end]: 90, + }), + }); + // size: 40 + const inForeign4: DraggableDimension = getDraggableDimension({ + id: 'inForeign4', + droppableId: foreign.id, + margin, + // pushed forward by margin of inForeign3 + clientRect: getClientRect({ + [axis.start]: 100, + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.end]: 140, + }), + }); - if (!result) { - throw new Error('invalid test setup'); - } - - it('should move to the start edge of the droppable', () => { - const expected: Position = moveToEdge({ - source: inHome1.page.withMargin, - sourceEdge: 'start', - destination: foreign.page.withMargin, - destinationEdge: 'start', - destinationAxis: foreign.axis, + const draggables: DraggableDimension[] = [ + inForeign1, inForeign2, inForeign3, inForeign4, + ]; + + it('should return null when the target is not within the list - cannot really happen', () => { + const result: ?Result = moveToNewDroppable({ + pageCenter: inHome1.page.withMargin.center, + draggable: inHome1, + target: inHome2, + destination: foreign, + insideDestination: draggables, + home: { + index: 0, + droppableId: home.id, + }, + }); + + expect(result).toBe(null); }); - expect(result.pageCenter).toEqual(expected); - }); + 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.id, + }, + }); - it('should return an empty impact', () => { - const expected: DragImpact = { - movement: { - draggables: [], - amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), - isBeyondStartPosition: false, - }, - direction: foreign.axis.direction, - destination: { - droppableId: foreign.id, - index: 0, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); + if (!result) { + throw new Error('invalid test setup'); + } - 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.id, - }, - }); + it('should move to the start edge of the droppable', () => { + const expected: Position = moveToEdge({ + source: inHome1.page.withMargin, + sourceEdge: 'start', + destination: foreign.page.withMargin, + destinationEdge: 'start', + destinationAxis: foreign.axis, + }); + + expect(result.pageCenter).toEqual(expected); + }); - 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 return an empty impact', () => { + const expected: DragImpact = { + movement: { + draggables: [], + amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), + isBeyondStartPosition: false, + }, + direction: foreign.axis.direction, + destination: { + droppableId: foreign.id, + index: 0, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - 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.id, + }, + }); - it('should move the target and everything below it forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - draggables: [inForeign2.id, inForeign3.id, inForeign4.id], - amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), - isBeyondStartPosition: false, - }, - direction: foreign.axis.direction, - destination: { - droppableId: foreign.id, - // index of foreign2 - index: 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); + if (!result) { + throw new Error('invalid test setup'); + } - 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.id, - }, - }); + 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); + }); - if (!result) { - throw new Error('invalid test setup'); - } - - it('should move under 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 the target and everything below it forward', () => { + const expected: DragImpact = { + movement: { + // ordered by closest impacted + draggables: [inForeign2.id, inForeign3.id, inForeign4.id], + amount: patch(foreign.axis.line, inHome1.page.withMargin[foreign.axis.size]), + isBeyondStartPosition: false, + }, + direction: foreign.axis.direction, + destination: { + droppableId: foreign.id, + // index of foreign2 + index: 1, + }, + }; + + expect(result.impact).toEqual(expected); + }); }); - 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.id, + }, + }); + + 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, + }); + + expect(result.pageCenter).toEqual(expected); + }); - it('should move everything after the proposed index forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - draggables: [inForeign3.id, inForeign4.id], - amount: patch(foreign.axis.line, inHome4.page.withMargin[foreign.axis.size]), - isBeyondStartPosition: false, - }, - direction: foreign.axis.direction, - destination: { - droppableId: foreign.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 + draggables: [inForeign3.id, inForeign4.id], + amount: patch(foreign.axis.line, inHome4.page.withMargin[foreign.axis.size]), + isBeyondStartPosition: false, + }, + direction: foreign.axis.direction, + destination: { + droppableId: foreign.id, + // going after target, so index is target index + 1 + index: 2, + }, + }; + + expect(result.impact).toEqual(expected); + }); + }); }); }); }); From e349ba72c5533ef7e87d9c76b91ea59a2c3bf22f Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 10:05:11 +1000 Subject: [PATCH 090/117] adding test for droppable dimension clipping --- .../droppable-dimension-publisher.jsx | 3 +- test/unit/state/get-drag-impact.spec.js | 2 - ...cted-droppable-dimension-publisher.spec.js | 182 ++++++++++++++++-- 3 files changed, 164 insertions(+), 23 deletions(-) diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index a0ab56e196..9782b265a8 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -57,7 +57,8 @@ export default class DroppableDimensionPublisher extends Component { return current; } - // need to trim dimensions + // Clipping the droppables dimensions by its scroll parent + const parent: ClientRect = this.closestScrollable.getBoundingClientRect(); const top = Math.max(current.top, parent.top); diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index eebc7cee02..dcade01432 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -24,8 +24,6 @@ const droppableId: DroppableId = 'drop-1'; const origin: Position = { x: 0, y: 0 }; describe('get drag impact', () => { - // TODO: add tests for when not in home list - describe('vertical', () => { const droppable: DroppableDimension = getDroppableDimension({ id: droppableId, diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js index b2ae39ba61..c86caffbf9 100644 --- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js @@ -15,7 +15,7 @@ import type { } from '../../../src/types'; const droppableId: DroppableId = 'drop-1'; -const dimension: DroppableDimension = getDroppableDimension({ +const droppable: DroppableDimension = getDroppableDimension({ id: droppableId, clientRect: getClientRect({ top: 0, @@ -113,12 +113,12 @@ describe('DraggableDimensionPublisher', () => { const publish = jest.fn(); const updateScroll = jest.fn(); jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ - top: dimension.page.withoutMargin.top, - bottom: dimension.page.withoutMargin.bottom, - left: dimension.page.withoutMargin.left, - right: dimension.page.withoutMargin.right, - height: dimension.page.withoutMargin.height, - width: dimension.page.withoutMargin.width, + top: droppable.page.withoutMargin.top, + bottom: droppable.page.withoutMargin.bottom, + left: droppable.page.withoutMargin.left, + right: droppable.page.withoutMargin.right, + height: droppable.page.withoutMargin.height, + width: droppable.page.withoutMargin.width, })); jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({ marginTop: '0', @@ -137,7 +137,7 @@ describe('DraggableDimensionPublisher', () => { shouldPublish: true, }); - expect(publish).toBeCalledWith(dimension); + expect(publish).toBeCalledWith(droppable); expect(publish).toHaveBeenCalledTimes(1); wrapper.unmount(); @@ -163,12 +163,12 @@ describe('DraggableDimensionPublisher', () => { margin, }); jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ - top: dimension.page.withoutMargin.top, - bottom: dimension.page.withoutMargin.bottom, - left: dimension.page.withoutMargin.left, - right: dimension.page.withoutMargin.right, - height: dimension.page.withoutMargin.height, - width: dimension.page.withoutMargin.width, + top: droppable.page.withoutMargin.top, + bottom: droppable.page.withoutMargin.bottom, + left: droppable.page.withoutMargin.left, + right: droppable.page.withoutMargin.right, + height: droppable.page.withoutMargin.height, + width: droppable.page.withoutMargin.width, })); jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({ marginTop: `${margin.top}`, @@ -290,12 +290,12 @@ describe('DraggableDimensionPublisher', () => { const publish = jest.fn(); const updateScroll = jest.fn(); jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ - top: dimension.page.withMargin.top, - bottom: dimension.page.withMargin.bottom, - left: dimension.page.withMargin.left, - right: dimension.page.withMargin.right, - height: dimension.page.withMargin.height, - width: dimension.page.withMargin.width, + top: droppable.page.withMargin.top, + bottom: droppable.page.withMargin.bottom, + left: droppable.page.withMargin.left, + right: droppable.page.withMargin.right, + height: droppable.page.withMargin.height, + width: droppable.page.withMargin.width, })); // initial publish @@ -330,6 +330,148 @@ describe('DraggableDimensionPublisher', () => { wrapper.unmount(); }); + + describe('dimension clipping', () => { + class ScrollParent extends Component { + props: {| + children: ?any + |} + + render() { + return ( +
+ { this.props.children } +
+ ); + } + } + + type ItemProps = { + publish: (dimension: DroppableDimension) => void, + updateScroll: (id: DroppableId, offset: Position) => void, + shouldPublish?: boolean, + }; + + class Item extends Component { + /* eslint-disable react/sort-comp */ + props: ItemProps + + state: {| + ref: ?HTMLElement + |} + + state = { + ref: null, + } + + setRef = (ref: ?HTMLElement) => { + this.setState({ + ref, + }); + } + + render() { + return ( +
+ {/* $ExpectError */ } + +
hello world
+
+
+ ); + } + } + + class App extends Component { + props: ItemProps + render() { + return ( + + + + ); + } + } + + it('should clip a dimension by the size of its scroll parent', () => { + const publish = jest.fn(); + const updateScroll = jest.fn(); + const scrollParentRect: ClientRect = getClientRect({ + top: 0, + bottom: 100, + left: 0, + right: 100, + }); + const expected = getDroppableDimension({ + id: droppableId, + clientRect: scrollParentRect, + }); + (() => { + let count = 0; + jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => { + // first call is item + if (count === 0) { + count++; + // 10px bigger in every direction from the scroll parent + return getClientRect({ + top: -10, + bottom: 110, + left: -10, + right: 110, + }); + } + + // second call is to the scroll parent + return scrollParentRect; + }); + })(); + + jest.spyOn(window, 'getComputedStyle').mockImplementation((el) => { + const noMargin = { + marginTop: '0', + marginRight: '0', + marginBottom: '0', + marginLeft: '0', + }; + + if (el.className === 'item') { + return noMargin; + } + + if (el.className === 'scroll-parent') { + return { + ...noMargin, + overflow: 'auto', + }; + } + + throw new Error('unknown el'); + }); + + const wrapper = mount( + + ); + wrapper.setProps({ + shouldPublish: true, + }); + + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + }); }); describe('scroll watching', () => { From 0bff636a430056288841114568874e5728df0cb9 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 10:15:38 +1000 Subject: [PATCH 091/117] fixing flow errors in test file --- ...cted-droppable-dimension-publisher.spec.js | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js index c86caffbf9..e0505767c7 100644 --- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js @@ -1,4 +1,5 @@ // @flow +/* eslint-disable react/no-multi-comp */ import React, { Component } from 'react'; import { mount } from 'enzyme'; import DroppableDimensionPublisher from '../../../src/view/droppable-dimension-publisher/droppable-dimension-publisher'; @@ -12,6 +13,7 @@ import type { DroppableDimension, HTMLElement, Position, + ReactElement, } from '../../../src/types'; const droppableId: DroppableId = 'drop-1'; @@ -332,10 +334,16 @@ describe('DraggableDimensionPublisher', () => { }); describe('dimension clipping', () => { + type ItemProps = { + publish: (dimension: DroppableDimension) => void, + updateScroll: (id: DroppableId, offset: Position) => void, + shouldPublish?: boolean, + }; + class ScrollParent extends Component { - props: {| - children: ?any - |} + props: { + children: ?ReactElement + } render() { return ( @@ -346,12 +354,6 @@ describe('DraggableDimensionPublisher', () => { } } - type ItemProps = { - publish: (dimension: DroppableDimension) => void, - updateScroll: (id: DroppableId, offset: Position) => void, - shouldPublish?: boolean, - }; - class Item extends Component { /* eslint-disable react/sort-comp */ props: ItemProps @@ -394,10 +396,15 @@ describe('DraggableDimensionPublisher', () => { class App extends Component { props: ItemProps + render() { return ( - + ); } From da5d620524a99e52fed9baff3377359f92614437 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 10:59:56 +1000 Subject: [PATCH 092/117] adding child visibility check to move-cross-axis/index --- src/state/move-cross-axis/index.js | 7 +- src/state/reducer.js | 5 +- .../droppable-dimension-publisher.jsx | 5 +- .../move-cross-axis/move-cross-axis.spec.js | 98 +++++++++++++++++++ test/utils/get-droppable-with-draggables.js | 7 +- 5 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 test/unit/state/move-cross-axis/move-cross-axis.spec.js diff --git a/src/state/move-cross-axis/index.js b/src/state/move-cross-axis/index.js index c7674762ed..69d712889c 100644 --- a/src/state/move-cross-axis/index.js +++ b/src/state/move-cross-axis/index.js @@ -13,7 +13,6 @@ import type { DraggableDimensionMap, DroppableDimensionMap, DraggableLocation, - DragImpact, } from '../../types'; type Args = {| @@ -26,8 +25,6 @@ type Args = {| droppableId: DroppableId, // the original location of the draggable home: DraggableLocation, - // the current drag impact - impact: DragImpact, // all the dimensions in the system draggables: DraggableDimensionMap, droppables: DroppableDimensionMap, @@ -40,7 +37,6 @@ export default ({ droppableId, home, draggables, - impact, droppables, }: Args): ?Result => { const draggable: DraggableDimension = draggables[draggableId]; @@ -79,11 +75,10 @@ export default ({ return moveToNewDroppable({ pageCenter, + destination, draggable, target, - destination, insideDestination, home, - impact, }); }; diff --git a/src/state/reducer.js b/src/state/reducer.js index 08370db745..15f3ca3a43 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -114,6 +114,7 @@ const move = ({ const current: CurrentDrag = { id: previous.id, type: previous.type, + isScrollAllowed: previous.isScrollAllowed, client, page, withinDroppable, @@ -228,7 +229,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { return state; } - const { id, type, client, page, windowScroll } = action.payload; + const { id, type, client, page, windowScroll, isScrollAllowed } = action.payload; // no scroll diff yet so withinDroppable is just the center position const withinDroppable: WithinDroppable = { @@ -273,6 +274,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { }, withinDroppable, windowScroll, + isScrollAllowed, shouldAnimate: false, }; @@ -504,7 +506,6 @@ export default (state: State = clean('IDLE'), action: Action): State => { draggableId, droppableId, home, - impact: state.drag.impact, draggables: state.dimension.draggable, droppables: state.dimension.droppable, }); diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 9782b265a8..5a54740c99 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -57,7 +57,10 @@ export default class DroppableDimensionPublisher extends Component { return current; } - // Clipping the droppables dimensions by its scroll parent + // Clipping the droppables dimensions by its scroll parent. + // If the scroll parent is smaller in size than the droppable we need to trim the + // dimensions of the droppable so that it is has the correct visible coordinates + // Still using the margin of the droppable. We can revisit the margin selection decision const parent: ClientRect = this.closestScrollable.getBoundingClientRect(); 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 new file mode 100644 index 0000000000..05ff26ee3c --- /dev/null +++ b/test/unit/state/move-cross-axis/move-cross-axis.spec.js @@ -0,0 +1,98 @@ +// @flow +import moveCrossAxis from '../../../../src/state/move-cross-axis/'; +import type { Result } from '../../../../src/state/move-cross-axis/move-cross-axis-types'; +import getDroppableWithDraggables from '../../../utils/get-droppable-with-draggables'; +import type { Result as Data } from '../../../utils/get-droppable-with-draggables'; +import type { + DraggableDimension, + DraggableDimensionMap, + DroppableDimensionMap, +} from '../../../../src/types'; + +// The functionality of move-cross-axis is covered by other files in this folder. +// This spec file is directed any any logic in move-cross-axis/index.js + +describe('move cross axis', () => { + const home: Data = getDroppableWithDraggables({ + droppableId: 'home', + droppableRect: { top: 0, left: 0, right: 100, bottom: 100 }, + draggableRects: [ + { top: 0, left: 0, right: 100, bottom: 20 }, + ], + }); + + it('should return null if there are draggables in a destination list but none are visible', () => { + const foreign: Data = getDroppableWithDraggables({ + droppableId: 'foreign', + // to the right of home + droppableRect: { top: 0, left: 100, right: 200, bottom: 100 }, + draggableRects: [ + // bigger than that visible rect + { top: 0, left: 100, right: 200, bottom: 110 }, + ], + }); + const draggables: DraggableDimensionMap = { + ...home.draggables, + ...foreign.draggables, + }; + const droppables: DroppableDimensionMap = { + [home.droppable.id]: home.droppable, + [foreign.droppable.id]: foreign.droppable, + }; + const draggable: DraggableDimension = home.draggableDimensions[0]; + + const result: ?Result = moveCrossAxis({ + isMovingForward: true, + pageCenter: draggable.page.withMargin.center, + draggableId: draggable.id, + droppableId: home.droppable.id, + home: { + droppableId: home.droppable.id, + index: 0, + }, + draggables, + droppables, + }); + + expect(result).toBe(null); + }); + + // this test is a validation that the previous test is working correctly + it('should return a droppable if its children are visible (and all other criteria are met', () => { + // adding visible child to foreign + const foreign: Data = getDroppableWithDraggables({ + droppableId: 'foreign', + // to the right of home + droppableRect: { top: 0, left: 100, right: 200, bottom: 100 }, + draggableRects: [ + // child is visible + { top: 0, left: 100, right: 200, bottom: 90 }, + ], + }); + const draggables: DraggableDimensionMap = { + ...home.draggables, + ...foreign.draggables, + }; + const droppables: DroppableDimensionMap = { + [home.droppable.id]: home.droppable, + [foreign.droppable.id]: foreign.droppable, + }; + const draggable: DraggableDimension = home.draggableDimensions[0]; + + const result: ?Result = moveCrossAxis({ + isMovingForward: true, + pageCenter: draggable.page.withMargin.center, + draggableId: draggable.id, + droppableId: home.droppable.id, + home: { + droppableId: home.droppable.id, + index: 0, + }, + draggables, + droppables, + }); + + // not asserting anything about the behaviour - just that something was returned + expect(result).toBeTruthy(); + }); +}); diff --git a/test/utils/get-droppable-with-draggables.js b/test/utils/get-droppable-with-draggables.js index c2aba81424..a6616f7a96 100644 --- a/test/utils/get-droppable-with-draggables.js +++ b/test/utils/get-droppable-with-draggables.js @@ -1,3 +1,4 @@ +// @flow import { getDraggableDimension, getDroppableDimension } from '../../src/state/dimension'; import type { ClientRect } from '../../src/state/dimension'; import type { @@ -9,17 +10,17 @@ import type { } from '../../src/types'; import getClientRect from './get-client-rect'; -type ClientRectSubset = { +type ClientRectSubset = {| top: number, left: number, right: number, bottom: number, -} +|} type Args = {| direction?: Direction, droppableId: DroppableId, - droppableRect: ClientRect, + droppableRect: ClientRectSubset, draggableRects: ClientRectSubset[], |}; From b6579c0c5ca9be753b0a12ae03c750cce90b0ecb Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 14:03:07 +1000 Subject: [PATCH 093/117] adding placeholder validation to connected droppable tests --- src/view/droppable/connected-droppable.js | 5 +- test/unit/view/connected-droppable.spec.js | 274 ++++++++++++++++++--- 2 files changed, 235 insertions(+), 44 deletions(-) diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index 23c84990f6..a98544eb8c 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -27,7 +27,6 @@ import type { } from './droppable-types'; export const makeSelector = (): Selector => { - const i = 0; const idSelector = (state: State, ownProps: OwnProps) => ownProps.droppableId; const isDropDisabledSelector = (state: State, ownProps: OwnProps) => @@ -101,7 +100,7 @@ export const makeSelector = (): Selector => { isDropDisabled: boolean, ): MapProps => { if (isDropDisabled) { - return getMapProps(false); + return getMapProps(false, null); } if (phase === 'DRAGGING') { @@ -127,7 +126,7 @@ export const makeSelector = (): Selector => { } const isDraggingOver = getIsDraggingOver(id, pending.impact.destination); - const placeholder = getPlaceholder( + const placeholder: ?Placeholder = getPlaceholder( id, pending.result.source, pending.result.destination, diff --git a/test/unit/view/connected-droppable.spec.js b/test/unit/view/connected-droppable.spec.js index 3eed72be00..10e6c1cad4 100644 --- a/test/unit/view/connected-droppable.spec.js +++ b/test/unit/view/connected-droppable.spec.js @@ -19,58 +19,72 @@ import type { DropResult, CurrentDrag, DraggableDimension, + DraggableLocation, InitialDragLocation, CurrentDragLocation, } from '../../../src/types'; -import type { MapProps, Provided, Selector } from '../../../src/view/droppable/droppable-types'; +import type { + MapProps, + Provided, + Selector, + Placeholder, +} from '../../../src/view/droppable/droppable-types'; type ExecuteArgs = {| id: DroppableId, phase: Phase, drag: ?DragState, pending: ?PendingDrop, - isDropDisabled?: boolean + draggable: ?DraggableDimension, + isDropDisabled: ?boolean, |} const execute = (selector: Selector) => - ({ phase, drag, pending, id, isDropDisabled = false }: ExecuteArgs) => + ({ phase, drag, draggable, pending, id, isDropDisabled = false }: ExecuteArgs) => selector.resultFunc( - phase, drag, pending, id, isDropDisabled, + phase, drag, draggable, pending, id, isDropDisabled, ); const defaultMapProps: MapProps = { isDraggingOver: false, + placeholder: null, }; const droppableId: DroppableId = 'drop-1'; +const foreignDroppableId: DroppableId = 'foreign-droppable'; const draggableId: DraggableId = 'drag-1'; const origin: Position = { x: 0, y: 0 }; type DragArgs = {| - isDraggingOver: boolean + isDraggingOver: false | 'home' | 'foreign' |} -const perform = (() => { - const dimension: DraggableDimension = getDraggableDimension({ - id: draggableId, - droppableId, - clientRect: getClientRect({ - top: 100, - left: 0, - right: 100, - bottom: 200, - }), - }); +const draggable: DraggableDimension = getDraggableDimension({ + id: draggableId, + droppableId, + clientRect: getClientRect({ + top: 100, + left: 0, + right: 100, + bottom: 200, + }), +}); + +const placeholder: Placeholder = { + width: draggable.page.withoutMargin.width, + height: draggable.page.withoutMargin.height, +}; +const perform = (() => { const initial: InitialDrag = (() => { const client: InitialDragLocation = { - selection: dimension.client.withoutMargin.center, - center: dimension.client.withoutMargin.center, + selection: draggable.client.withoutMargin.center, + center: draggable.client.withoutMargin.center, }; const page: InitialDragLocation = { - selection: dimension.page.withoutMargin.center, - center: dimension.page.withoutMargin.center, + selection: draggable.page.withoutMargin.center, + center: draggable.page.withoutMargin.center, }; const value: InitialDrag = { @@ -111,30 +125,59 @@ const perform = (() => { center: page.center, }, shouldAnimate: true, + isScrollAllowed: true, }; return value; })(); - const dragOverImpact: DragImpact = { + const homeDestination: DraggableLocation = { + index: initial.source.index + 1, + droppableId, + }; + const foreignDestination: DraggableLocation = { + index: 0, + droppableId: foreignDroppableId, + }; + + const dragOverHomeImpact: DragImpact = { movement: { draggables: [draggableId], amount: { - y: dimension.page.withMargin.height, + y: draggable.page.withMargin.height, x: 0, }, isBeyondStartPosition: true, }, direction: 'vertical', - destination: { - index: initial.source.index + 1, - droppableId, + destination: homeDestination, + }; + + const dragOverForeignImpact: DragImpact = { + movement: { + draggables: [], + amount: { + y: draggable.page.withMargin.height, + x: 0, + }, + isBeyondStartPosition: false, }, + direction: 'vertical', + destination: foreignDestination, }; const drag = ({ isDraggingOver }: DragArgs): DragState => { + const impact: DragImpact = (() => { + if (isDraggingOver === 'home') { + return dragOverHomeImpact; + } + if (isDraggingOver === 'foreign') { + return dragOverForeignImpact; + } + return noImpact; + })(); const state: DragState = { current, - impact: isDraggingOver ? dragOverImpact : noImpact, + impact, initial, }; return state; @@ -142,25 +185,34 @@ const perform = (() => { const drop = ({ isDraggingOver }: DragArgs): PendingDrop => { // some made up position - const newHomeOffset: Position = { + const dontCare: Position = { x: 100, y: 20, }; + const destination: ?DraggableLocation = (() => { + if (isDraggingOver === 'home') { + return homeDestination; + } + if (isDraggingOver === 'foreign') { + return foreignDestination; + } + return null; + })(); + + const impact: DragImpact = drag({ isDraggingOver }).impact; + const result: DropResult = { draggableId, type: 'TYPE', source: initial.source, - destination: { - index: initial.source.index + 1, - droppableId: initial.source.droppableId, - }, + destination, }; const pending: PendingDrop = { trigger: 'DROP', - newHomeOffset, - impact: isDraggingOver ? dragOverImpact : noImpact, + newHomeOffset: dontCare, + impact, result, }; @@ -189,6 +241,7 @@ describe('Droppable - connected', () => { phase, drag: null, pending: null, + draggable: null, id: droppableId, isDropDisabled: true, }); @@ -205,19 +258,49 @@ describe('Droppable - connected', () => { phase, drag: null, pending: null, + draggable: null, id: droppableId, + isDropDisabled: true, }); const second: MapProps = execute(selector)({ phase, drag: null, pending: null, + draggable: null, id: droppableId, + isDropDisabled: true, }); // checking object equality expect(first).toBe(second); }); }); + + it('should not break memoization between phases', () => { + let previous: MapProps; + const selector = makeSelector(); + + phases.forEach((phase: Phase) => { + const result: MapProps = execute(selector)({ + phase, + drag: null, + pending: null, + draggable: null, + id: droppableId, + isDropDisabled: true, + }); + + // seed previous + if (!previous) { + previous = result; + return; + } + + // checking object equality + expect(result).toBe(previous); + expect(result).toEqual(defaultMapProps); + }); + }); }); describe('while dragging', () => { @@ -225,6 +308,7 @@ describe('Droppable - connected', () => { const props: MapProps = execute(makeSelector())({ phase: 'DRAGGING', drag: null, + draggable: null, pending: null, id: droppableId, }); @@ -233,15 +317,17 @@ describe('Droppable - connected', () => { expect(console.error).toHaveBeenCalled(); }); - describe('dragging over', () => { + describe('over home droppable', () => { it('should return that it is dragging over', () => { const expected: MapProps = { isDraggingOver: true, + placeholder: null, }; const props: MapProps = execute(makeSelector())({ phase: 'DRAGGING', - drag: perform.drag({ isDraggingOver: true }), + drag: perform.drag({ isDraggingOver: 'home' }), + draggable, pending: null, id: droppableId, }); @@ -253,18 +339,21 @@ describe('Droppable - connected', () => { const selector = makeSelector(); const expected: MapProps = { isDraggingOver: true, + placeholder: null, }; const props1: MapProps = execute(selector)({ phase: 'DRAGGING', - drag: perform.drag({ isDraggingOver: true }), + drag: perform.drag({ isDraggingOver: 'home' }), pending: null, + draggable, id: droppableId, }); const props2: MapProps = execute(selector)({ phase: 'DRAGGING', - drag: perform.drag({ isDraggingOver: true }), + drag: perform.drag({ isDraggingOver: 'home' }), pending: null, + draggable, id: droppableId, }); @@ -275,10 +364,59 @@ describe('Droppable - connected', () => { }); }); + describe('over foreign droppable', () => { + it('should return that it is dragging over and a placeholder', () => { + const expected: MapProps = { + isDraggingOver: true, + placeholder, + }; + + const props: MapProps = execute(makeSelector())({ + phase: 'DRAGGING', + drag: perform.drag({ isDraggingOver: 'foreign' }), + draggable, + pending: null, + id: foreignDroppableId, + }); + + expect(props).toEqual(expected); + }); + + it('should not break memoization on multiple drags', () => { + const selector = makeSelector(); + const expected: MapProps = { + isDraggingOver: true, + placeholder, + }; + + const props1: MapProps = execute(selector)({ + phase: 'DRAGGING', + drag: perform.drag({ isDraggingOver: 'foreign' }), + pending: null, + draggable, + id: foreignDroppableId, + }); + const props2: MapProps = execute(selector)({ + phase: 'DRAGGING', + drag: perform.drag({ isDraggingOver: 'foreign' }), + pending: null, + draggable, + id: foreignDroppableId, + }); + + // checking object equality + expect(props1).toBe(props2); + expect(props1.placeholder).toBe(props2.placeholder); + expect(props1).toEqual(expected); + expect(props2).toEqual(expected); + }); + }); + describe('not dragging over', () => { it('should return that it is not dragging over', () => { const expected: MapProps = { isDraggingOver: false, + placeholder: null, }; const props: MapProps = execute(makeSelector())({ @@ -295,6 +433,7 @@ describe('Droppable - connected', () => { const selector = makeSelector(); const expected: MapProps = { isDraggingOver: false, + placeholder: null, }; const props1: MapProps = execute(selector)({ @@ -331,16 +470,17 @@ describe('Droppable - connected', () => { expect(console.error).toHaveBeenCalled(); }); - describe('dragging over', () => { + describe('was dragging over home droppable', () => { it('should return that it is dragging over', () => { const expected: MapProps = { isDraggingOver: true, + placeholder: null, }; const props: MapProps = execute(makeSelector())({ phase: 'DROP_ANIMATING', drag: null, - pending: perform.drop({ isDraggingOver: true }), + pending: perform.drop({ isDraggingOver: 'home' }), id: droppableId, }); @@ -351,18 +491,19 @@ describe('Droppable - connected', () => { const selector = makeSelector(); const expected: MapProps = { isDraggingOver: true, + placeholder: null, }; const dragging: MapProps = execute(selector)({ phase: 'DRAGGING', - drag: perform.drag({ isDraggingOver: true }), + drag: perform.drag({ isDraggingOver: 'home' }), pending: null, id: droppableId, }); const dropAnimating: MapProps = execute(selector)({ phase: 'DROP_ANIMATING', drag: null, - pending: perform.drop({ isDraggingOver: true }), + pending: perform.drop({ isDraggingOver: 'home' }), id: droppableId, }); @@ -373,15 +514,64 @@ describe('Droppable - connected', () => { }); }); + describe('was dragging over foreign droppable', () => { + it('should return that it is dragging over and provide a placeholder', () => { + const expected: MapProps = { + isDraggingOver: true, + placeholder, + }; + + const props: MapProps = execute(makeSelector())({ + phase: 'DROP_ANIMATING', + drag: null, + pending: perform.drop({ isDraggingOver: 'foreign' }), + draggable, + id: foreignDroppableId, + }); + + expect(props).toEqual(expected); + }); + + it('should not break memoization from a previous DRAGGING phase', () => { + const selector = makeSelector(); + const expected: MapProps = { + isDraggingOver: true, + placeholder, + }; + + const dragging: MapProps = execute(selector)({ + phase: 'DRAGGING', + drag: perform.drag({ isDraggingOver: 'foreign' }), + pending: null, + draggable, + id: foreignDroppableId, + }); + const dropAnimating: MapProps = execute(selector)({ + phase: 'DROP_ANIMATING', + drag: null, + pending: perform.drop({ isDraggingOver: 'foreign' }), + draggable, + id: foreignDroppableId, + }); + + expect(dragging).toEqual(expected); + expect(dropAnimating).toEqual(expected); + // checking object equality + expect(dragging).toBe(dropAnimating); + }); + }); + describe('not dragging over', () => { it('should return that it is not dragging over', () => { const expected: MapProps = { isDraggingOver: false, + placeholder: null, }; const props: MapProps = execute(makeSelector())({ phase: 'DROP_ANIMATING', drag: null, + draggable, pending: perform.drop({ isDraggingOver: false }), id: droppableId, }); @@ -393,12 +583,14 @@ describe('Droppable - connected', () => { const selector = makeSelector(); const expected: MapProps = { isDraggingOver: false, + placeholder: null, }; const dragging: MapProps = execute(selector)({ phase: 'DRAGGING', drag: perform.drag({ isDraggingOver: false }), pending: null, + draggable, id: droppableId, }); const dropAnimating: MapProps = execute(selector)({ From 2165d79a418a345305341f4a52cc2843d2a97bd0 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 14:10:39 +1000 Subject: [PATCH 094/117] cleaning up tests --- test/unit/view/connected-droppable.spec.js | 89 +++++++++++++--------- 1 file changed, 51 insertions(+), 38 deletions(-) diff --git a/test/unit/view/connected-droppable.spec.js b/test/unit/view/connected-droppable.spec.js index 10e6c1cad4..6ade9030f0 100644 --- a/test/unit/view/connected-droppable.spec.js +++ b/test/unit/view/connected-droppable.spec.js @@ -31,18 +31,19 @@ import type { } from '../../../src/view/droppable/droppable-types'; type ExecuteArgs = {| - id: DroppableId, + droppableId: DroppableId, phase: Phase, drag: ?DragState, pending: ?PendingDrop, draggable: ?DraggableDimension, - isDropDisabled: ?boolean, + // being simple and defaulting to false (droppable is enabled) + isDropDisabled?: boolean, |} const execute = (selector: Selector) => - ({ phase, drag, draggable, pending, id, isDropDisabled = false }: ExecuteArgs) => + ({ phase, drag, draggable, pending, droppableId, isDropDisabled = false }: ExecuteArgs) => selector.resultFunc( - phase, drag, draggable, pending, id, isDropDisabled, + phase, drag, draggable, pending, droppableId, isDropDisabled, ); const defaultMapProps: MapProps = { @@ -50,7 +51,7 @@ const defaultMapProps: MapProps = { placeholder: null, }; -const droppableId: DroppableId = 'drop-1'; +const homeDroppableId: DroppableId = 'home-id'; const foreignDroppableId: DroppableId = 'foreign-droppable'; const draggableId: DraggableId = 'drag-1'; const origin: Position = { x: 0, y: 0 }; @@ -61,7 +62,7 @@ type DragArgs = {| const draggable: DraggableDimension = getDraggableDimension({ id: draggableId, - droppableId, + droppableId: homeDroppableId, clientRect: getClientRect({ top: 100, left: 0, @@ -90,7 +91,7 @@ const perform = (() => { const value: InitialDrag = { source: { index: 0, - droppableId, + droppableId: homeDroppableId, }, client, page, @@ -99,6 +100,7 @@ const perform = (() => { center: page.center, }, }; + return value; })(); @@ -132,7 +134,7 @@ const perform = (() => { const homeDestination: DraggableLocation = { index: initial.source.index + 1, - droppableId, + droppableId: homeDroppableId, }; const foreignDestination: DraggableLocation = { index: 0, @@ -242,7 +244,7 @@ describe('Droppable - connected', () => { drag: null, pending: null, draggable: null, - id: droppableId, + droppableId: homeDroppableId, isDropDisabled: true, }); @@ -259,7 +261,7 @@ describe('Droppable - connected', () => { drag: null, pending: null, draggable: null, - id: droppableId, + droppableId: homeDroppableId, isDropDisabled: true, }); const second: MapProps = execute(selector)({ @@ -267,7 +269,7 @@ describe('Droppable - connected', () => { drag: null, pending: null, draggable: null, - id: droppableId, + droppableId: homeDroppableId, isDropDisabled: true, }); @@ -286,7 +288,7 @@ describe('Droppable - connected', () => { drag: null, pending: null, draggable: null, - id: droppableId, + droppableId: homeDroppableId, isDropDisabled: true, }); @@ -310,7 +312,7 @@ describe('Droppable - connected', () => { drag: null, draggable: null, pending: null, - id: droppableId, + droppableId: homeDroppableId, }); expect(props).toEqual(defaultMapProps); @@ -329,7 +331,7 @@ describe('Droppable - connected', () => { drag: perform.drag({ isDraggingOver: 'home' }), draggable, pending: null, - id: droppableId, + droppableId: homeDroppableId, }); expect(props).toEqual(expected); @@ -347,14 +349,14 @@ describe('Droppable - connected', () => { drag: perform.drag({ isDraggingOver: 'home' }), pending: null, draggable, - id: droppableId, + droppableId: homeDroppableId, }); const props2: MapProps = execute(selector)({ phase: 'DRAGGING', drag: perform.drag({ isDraggingOver: 'home' }), pending: null, draggable, - id: droppableId, + droppableId: homeDroppableId, }); // checking object equality @@ -376,7 +378,7 @@ describe('Droppable - connected', () => { drag: perform.drag({ isDraggingOver: 'foreign' }), draggable, pending: null, - id: foreignDroppableId, + droppableId: foreignDroppableId, }); expect(props).toEqual(expected); @@ -394,14 +396,14 @@ describe('Droppable - connected', () => { drag: perform.drag({ isDraggingOver: 'foreign' }), pending: null, draggable, - id: foreignDroppableId, + droppableId: foreignDroppableId, }); const props2: MapProps = execute(selector)({ phase: 'DRAGGING', drag: perform.drag({ isDraggingOver: 'foreign' }), pending: null, draggable, - id: foreignDroppableId, + droppableId: foreignDroppableId, }); // checking object equality @@ -423,7 +425,8 @@ describe('Droppable - connected', () => { phase: 'DRAGGING', drag: perform.drag({ isDraggingOver: false }), pending: null, - id: droppableId, + draggable, + droppableId: homeDroppableId, }); expect(props).toEqual(expected); @@ -440,13 +443,15 @@ describe('Droppable - connected', () => { phase: 'DRAGGING', drag: perform.drag({ isDraggingOver: false }), pending: null, - id: droppableId, + draggable, + droppableId: homeDroppableId, }); const props2: MapProps = execute(selector)({ phase: 'DRAGGING', drag: perform.drag({ isDraggingOver: false }), pending: null, - id: droppableId, + draggable, + droppableId: homeDroppableId, }); // checking object equality @@ -463,7 +468,8 @@ describe('Droppable - connected', () => { phase: 'DROP_ANIMATING', drag: null, pending: null, - id: droppableId, + draggable, + droppableId: homeDroppableId, }); expect(props).toEqual(defaultMapProps); @@ -480,8 +486,9 @@ describe('Droppable - connected', () => { const props: MapProps = execute(makeSelector())({ phase: 'DROP_ANIMATING', drag: null, + draggable, pending: perform.drop({ isDraggingOver: 'home' }), - id: droppableId, + droppableId: homeDroppableId, }); expect(props).toEqual(expected); @@ -498,13 +505,15 @@ describe('Droppable - connected', () => { phase: 'DRAGGING', drag: perform.drag({ isDraggingOver: 'home' }), pending: null, - id: droppableId, + draggable, + droppableId: homeDroppableId, }); const dropAnimating: MapProps = execute(selector)({ phase: 'DROP_ANIMATING', drag: null, pending: perform.drop({ isDraggingOver: 'home' }), - id: droppableId, + draggable, + droppableId: homeDroppableId, }); expect(dragging).toEqual(expected); @@ -526,7 +535,7 @@ describe('Droppable - connected', () => { drag: null, pending: perform.drop({ isDraggingOver: 'foreign' }), draggable, - id: foreignDroppableId, + droppableId: foreignDroppableId, }); expect(props).toEqual(expected); @@ -544,14 +553,14 @@ describe('Droppable - connected', () => { drag: perform.drag({ isDraggingOver: 'foreign' }), pending: null, draggable, - id: foreignDroppableId, + droppableId: foreignDroppableId, }); const dropAnimating: MapProps = execute(selector)({ phase: 'DROP_ANIMATING', drag: null, pending: perform.drop({ isDraggingOver: 'foreign' }), draggable, - id: foreignDroppableId, + droppableId: foreignDroppableId, }); expect(dragging).toEqual(expected); @@ -573,7 +582,7 @@ describe('Droppable - connected', () => { drag: null, draggable, pending: perform.drop({ isDraggingOver: false }), - id: droppableId, + droppableId: homeDroppableId, }); expect(props).toEqual(expected); @@ -591,13 +600,14 @@ describe('Droppable - connected', () => { drag: perform.drag({ isDraggingOver: false }), pending: null, draggable, - id: droppableId, + droppableId: homeDroppableId, }); const dropAnimating: MapProps = execute(selector)({ phase: 'DROP_ANIMATING', drag: null, pending: perform.drop({ isDraggingOver: false }), - id: droppableId, + draggable, + droppableId: homeDroppableId, }); expect(dragging).toEqual(expected); @@ -609,17 +619,18 @@ describe('Droppable - connected', () => { }); describe('other phases', () => { - const other: Phase[] = ['IDLE', 'COLLECTING_DIMENSIONS', 'DROP_COMPLETE']; + const others: Phase[] = ['IDLE', 'COLLECTING_DIMENSIONS', 'DROP_COMPLETE']; it('should return the default props', () => { const selector = makeSelector(); - other.forEach((phase: Phase): void => { + others.forEach((phase: Phase): void => { const props: MapProps = execute(selector)({ phase, drag: null, pending: null, - id: droppableId, + draggable: null, + droppableId: homeDroppableId, }); expect(props).toEqual(defaultMapProps); @@ -629,18 +640,20 @@ describe('Droppable - connected', () => { it('should not break memoization on multiple calls', () => { const selector = makeSelector(); - other.forEach((phase: Phase): void => { + others.forEach((phase: Phase): void => { const first: MapProps = execute(selector)({ phase, drag: null, pending: null, - id: droppableId, + draggable: null, + droppableId: homeDroppableId, }); const second: MapProps = execute(selector)({ phase, drag: null, pending: null, - id: droppableId, + draggable: null, + droppableId: homeDroppableId, }); expect(first).toEqual(defaultMapProps); From 67b8895e101b91494cae37368aecc9f9bb1f2129 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 15:50:36 +1000 Subject: [PATCH 095/117] adding tests for publishing whether a droppable is enabling during a drag --- .../droppable-dimension-publisher-types.js | 12 +- .../droppable-dimension-publisher.jsx | 41 ++- test/unit/view/connected-draggable.spec.js | 1 + ...cted-droppable-dimension-publisher.spec.js | 2 +- ...cted-droppable-dimension-publisher.spec.js | 312 +++++++++++------- 5 files changed, 225 insertions(+), 143 deletions(-) diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js b/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js index 58ad949bc1..f9012f3d33 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js @@ -14,9 +14,9 @@ export type MapProps = {| |} export type DispatchProps = {| - publish: (dimension: DroppableDimension) => mixed, - updateIsEnabled: (id: DroppableId, isEnabled: boolean) => mixed, - updateScroll: (id: DroppableId, offset: Position) => mixed, + publish: (dimension: DroppableDimension) => void, + updateIsEnabled: (id: DroppableId, isEnabled: boolean) => void, + updateScroll: (id: DroppableId, offset: Position) => void, |} export type OwnProps = {| @@ -28,7 +28,11 @@ export type OwnProps = {| children?: ReactElement, |} -export type Props = MapProps & DispatchProps & OwnProps; +export type Props = { + ...MapProps, + ...DispatchProps, + ...OwnProps +} // Having issues getting the correct reselect type // export type Selector = OutputSelector; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 5a54740c99..088d02fa6f 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -141,33 +141,44 @@ export default class DroppableDimensionPublisher extends Component { this.closestScrollable.removeEventListener('scroll', this.onClosestScroll); } - // TODO: componentDidUpdate? componentWillReceiveProps(nextProps: Props) { - if (nextProps.targetRef !== this.props.targetRef) { - if (this.isWatchingScroll) { - console.warn('changing targetRef while watching scroll!'); - this.unwatchScroll(); - } - } - - if (nextProps.isDropDisabled !== this.props.isDropDisabled) { - this.props.updateIsEnabled(this.props.droppableId, !nextProps.isDropDisabled); - } - // Because the dimension publisher wraps children - it might render even when its props do // not change. We need to ensure that it does not publish when it should not. - const shouldPublish = !this.props.shouldPublish && nextProps.shouldPublish; + + const shouldStartPublishing = !this.props.shouldPublish && nextProps.shouldPublish; + const alreadyPublishing = this.props.shouldPublish && nextProps.shouldPublish; + const stopPublishing = this.props.shouldPublish && !nextProps.shouldPublish; // should no longer watch for scrolling - if (!nextProps.shouldPublish) { + if (stopPublishing) { this.unwatchScroll(); return; } - if (!shouldPublish) { + if (alreadyPublishing) { + // if ref changes and watching scroll - unwatch the scroll + if (nextProps.targetRef !== this.props.targetRef) { + if (this.isWatchingScroll) { + console.warn('changing targetRef while watching scroll!'); + this.unwatchScroll(); + } + } + + // publish any changes to the disabled flag + if (nextProps.isDropDisabled !== this.props.isDropDisabled) { + this.props.updateIsEnabled(this.props.droppableId, !nextProps.isDropDisabled); + } + + return; + } + + // This will be the default when nothing is happening + if (!shouldStartPublishing) { return; } + // Need to start publishing + // discovering the closest scrollable for a drag this.closestScrollable = getClosestScrollable(this.props.targetRef); this.props.publish(this.getDimension()); diff --git a/test/unit/view/connected-draggable.spec.js b/test/unit/view/connected-draggable.spec.js index 57808ac280..1f327ff20a 100644 --- a/test/unit/view/connected-draggable.spec.js +++ b/test/unit/view/connected-draggable.spec.js @@ -99,6 +99,7 @@ const make = (() => { center: page.center, }, shouldAnimate: true, + isScrollAllowed: true, }; const state: DragState = { diff --git a/test/unit/view/connected-droppable-dimension-publisher.spec.js b/test/unit/view/connected-droppable-dimension-publisher.spec.js index 2aa874e986..79d9b9f6f3 100644 --- a/test/unit/view/connected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/connected-droppable-dimension-publisher.spec.js @@ -10,7 +10,7 @@ const shouldPublishMapProps: MapProps = { shouldPublish: true, }; -describe('Dimension publisher - connected', () => { +describe('Connected droppable dimension publisher', () => { it('should return the default props when not requested to publish dimensions', () => { const selector = makeSelector(); diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js index e0505767c7..de7bdd2d4e 100644 --- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js @@ -30,9 +30,14 @@ const droppable: DroppableDimension = getDroppableDimension({ class ScrollableItem extends Component { /* eslint-disable react/sort-comp */ props: { + // dispatch props publish: (dimension: DroppableDimension) => void, updateScroll: (id: DroppableId, offset: Position) => void, + updateIsEnabled: (id: DroppableId, isEnabled: boolean) => void, + // map props (default: false) shouldPublish?: boolean, + // scrollable item prop (default: false) + isDropDisabled?: boolean, } state: {| @@ -51,13 +56,15 @@ class ScrollableItem extends Component { render() { return ( - // $ExpectError - for an unknown reason flow is having a hard time with this
{ x: window.pageXOffset, y: window.pageYOffset, }; + let publish; + let updateScroll; + let updateIsEnabled; + let dispatchProps; + let wrapper; + + beforeEach(() => { + publish = jest.fn(); + updateScroll = jest.fn(); + updateIsEnabled = jest.fn(); + dispatchProps = { + publish, updateScroll, updateIsEnabled, + }; + }); afterEach(() => { // clean up any stubs @@ -87,18 +108,16 @@ describe('DraggableDimensionPublisher', () => { window.getComputedStyle.mockRestore(); } setWindowScroll(originalWindowScroll, { shouldPublish: false }); + + if (wrapper) { + wrapper.unmount(); + } }); describe('dimension publishing', () => { it('should not publish if not asked to', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); - - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ @@ -107,13 +126,10 @@ describe('DraggableDimensionPublisher', () => { expect(publish).not.toHaveBeenCalled(); expect(updateScroll).not.toHaveBeenCalled(); - - wrapper.unmount(); + expect(updateIsEnabled).not.toHaveBeenCalled(); }); it('should publish the dimensions of the target', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ top: droppable.page.withoutMargin.top, bottom: droppable.page.withoutMargin.bottom, @@ -129,11 +145,8 @@ describe('DraggableDimensionPublisher', () => { marginLeft: '0', })); - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ shouldPublish: true, @@ -141,8 +154,6 @@ describe('DraggableDimensionPublisher', () => { expect(publish).toBeCalledWith(droppable); expect(publish).toHaveBeenCalledTimes(1); - - wrapper.unmount(); }); it('should consider any margins when calculating dimensions', () => { @@ -152,8 +163,6 @@ describe('DraggableDimensionPublisher', () => { bottom: 40, left: 50, }; - const publish = jest.fn(); - const updateScroll = jest.fn(); const expected: DroppableDimension = getDroppableDimension({ id: droppableId, clientRect: getClientRect({ @@ -179,24 +188,17 @@ describe('DraggableDimensionPublisher', () => { marginLeft: `${margin.left}`, })); - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ shouldPublish: true, }); expect(publish).toBeCalledWith(expected); - - wrapper.unmount(); }); it('should consider the window scroll when calculating dimensions', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); const windowScroll: Position = { x: 500, y: 1000, @@ -220,11 +222,8 @@ describe('DraggableDimensionPublisher', () => { marginBottom: '0', marginLeft: '0', })); - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ @@ -232,13 +231,9 @@ describe('DraggableDimensionPublisher', () => { }); expect(publish).toHaveBeenCalledWith(expected); - - wrapper.unmount(); }); it('should consider the closest scrollable when calculating dimensions', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); const closestScroll: Position = { x: 500, y: 1000, @@ -268,11 +263,8 @@ describe('DraggableDimensionPublisher', () => { marginBottom: '0', marginLeft: '0', })); - const wrapper = mount( - , + wrapper = mount( + , ); // setting initial scroll const container: HTMLElement = wrapper.getDOMNode(); @@ -284,13 +276,9 @@ describe('DraggableDimensionPublisher', () => { }); expect(publish).toHaveBeenCalledWith(expected); - - wrapper.unmount(); }); it('should not publish unless it is freshly required to do so', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ top: droppable.page.withMargin.top, bottom: droppable.page.withMargin.bottom, @@ -301,11 +289,8 @@ describe('DraggableDimensionPublisher', () => { })); // initial publish - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ shouldPublish: true, @@ -329,14 +314,13 @@ describe('DraggableDimensionPublisher', () => { // just being extra safe: expect(updateScroll).not.toHaveBeenCalled(); - - wrapper.unmount(); }); describe('dimension clipping', () => { type ItemProps = { publish: (dimension: DroppableDimension) => void, updateScroll: (id: DroppableId, offset: Position) => void, + updateIsEnabled: (id: DroppableId, isEnabled: boolean) => void, shouldPublish?: boolean, }; @@ -378,14 +362,16 @@ describe('DraggableDimensionPublisher', () => { ref={this.setRef} className="item" > - {/* $ExpectError */ }
hello world
@@ -404,6 +390,7 @@ describe('DraggableDimensionPublisher', () => { publish={this.props.publish} updateScroll={this.props.updateScroll} shouldPublish={this.props.shouldPublish} + updateIsEnabled={this.props.updateIsEnabled} /> ); @@ -411,8 +398,6 @@ describe('DraggableDimensionPublisher', () => { } it('should clip a dimension by the size of its scroll parent', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); const scrollParentRect: ClientRect = getClientRect({ top: 0, bottom: 100, @@ -465,10 +450,11 @@ describe('DraggableDimensionPublisher', () => { throw new Error('unknown el'); }); - const wrapper = mount( + wrapper = mount( ); wrapper.setProps({ @@ -483,13 +469,8 @@ describe('DraggableDimensionPublisher', () => { describe('scroll watching', () => { it('should not publish any scroll changes unless told it can publish', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); - const wrapper = mount( - , + wrapper = mount( + , ); const container: HTMLElement = wrapper.getDOMNode(); @@ -507,18 +488,11 @@ describe('DraggableDimensionPublisher', () => { requestAnimationFrame.flush(); expect(updateScroll).not.toHaveBeenCalled(); - - wrapper.unmount(); }); it('should publish the closest scrollable scroll offset', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ shouldPublish: true, @@ -538,18 +512,11 @@ describe('DraggableDimensionPublisher', () => { expect(updateScroll.mock.calls[0]).toEqual([ droppableId, { x: 500, y: 1000 }, ]); - - wrapper.unmount(); }); it('should throttle multiple scrolls into a animation frame', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ shouldPublish: true, @@ -577,19 +544,12 @@ describe('DraggableDimensionPublisher', () => { // also checking that no loose frames are stored up requestAnimationFrame.flush(); expect(updateScroll).toHaveBeenCalledTimes(1); - - wrapper.unmount(); }); 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 publish = jest.fn(); - const updateScroll = jest.fn(); - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ shouldPublish: true, @@ -619,19 +579,12 @@ describe('DraggableDimensionPublisher', () => { requestAnimationFrame.step(); expect(updateScroll).toHaveBeenCalledTimes(1); - - wrapper.unmount(); }); it('should stop watching scroll when no longer required to publish', () => { // this can happen if you scroll backward and forward super quick - const publish = jest.fn(); - const updateScroll = jest.fn(); - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ shouldPublish: true, @@ -656,18 +609,11 @@ describe('DraggableDimensionPublisher', () => { // let any frames go that want to requestAnimationFrame.flush(); expect(updateScroll).toHaveBeenCalledTimes(1); - - wrapper.unmount(); }); it('should not publish a scroll update after requested not to update while an animation frame is occurring', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ shouldPublish: true, @@ -694,18 +640,11 @@ describe('DraggableDimensionPublisher', () => { requestAnimationFrame.flush(); expect(updateScroll).toHaveBeenCalledTimes(1); - - wrapper.unmount(); }); it('should stop watching for scroll events when the component is unmounted', () => { - const publish = jest.fn(); - const updateScroll = jest.fn(); - const wrapper = mount( - , + wrapper = mount( + , ); wrapper.setProps({ shouldPublish: true, @@ -721,7 +660,7 @@ describe('DraggableDimensionPublisher', () => { wrapper.unmount(); - // second event + // second event - will not fire any updates container.scrollTop = 1001; container.scrollLeft = 501; container.dispatchEvent(new Event('scroll')); @@ -729,4 +668,131 @@ describe('DraggableDimensionPublisher', () => { expect(updateScroll).toHaveBeenCalledTimes(1); }); }); + + describe('is enabled flag publishing', () => { + beforeEach(() => { + jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ + top: droppable.page.withoutMargin.top, + bottom: droppable.page.withoutMargin.bottom, + left: droppable.page.withoutMargin.left, + right: droppable.page.withoutMargin.right, + height: droppable.page.withoutMargin.height, + width: droppable.page.withoutMargin.width, + })); + jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({ + marginTop: '0', + marginRight: '0', + marginBottom: '0', + marginLeft: '0', + })); + }); + + it('should publish whether the droppable is enabled when requested to publish', () => { + describe('enabled on mount', () => { + it('should publish that the dimension is enabled', () => { + wrapper = mount( + , + ); + wrapper.setProps({ + shouldPublish: true, + }); + + expect(publish).toBeCalledWith(droppable); + expect(publish).toHaveBeenCalledTimes(1); + }); + }); + + describe('disabled on mount', () => { + it('should publish that the dimension is disabled', () => { + const expected = { + ...droppable, + isEnabled: false, + }; + + wrapper = mount( + , + ); + wrapper.setProps({ + shouldPublish: true, + }); + + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + }); + }); + + it('should publish changes to the enabled state of the droppable during a drag', () => { + wrapper = mount( + , + ); + + // initial publish + wrapper.setProps({ + shouldPublish: true, + }); + expect(publish.mock.calls[0][0].isEnabled).toBe(true); + + // disable + wrapper.setProps({ + isDropDisabled: true, + }); + expect(updateIsEnabled).toHaveBeenCalledTimes(1); + expect(updateIsEnabled.mock.calls[0]).toEqual([droppable.id, false]); + + // enable + wrapper.setProps({ + isDropDisabled: false, + }); + expect(updateIsEnabled).toHaveBeenCalledTimes(2); + expect(updateIsEnabled.mock.calls[1]).toEqual([droppable.id, true]); + }); + + it('should not publish changes to the enabled state of the droppable when a drag is not occuring', () => { + wrapper = mount( + , + ); + const change = () => { + // disabling + wrapper.setProps({ + isDropDisabled: true, + }); + // enabling + wrapper.setProps({ + isDropDisabled: false, + }); + }; + // not publishing yet + change(); + expect(updateIsEnabled).not.toHaveBeenCalled(); + + // now publishing + wrapper.setProps({ + shouldPublish: true, + }); + + // this change will now trigger an update x 2 + change(); + expect(updateIsEnabled).toHaveBeenCalledTimes(2); + // disabling + expect(updateIsEnabled.mock.calls[0]).toEqual([droppable.id, false]); + // enabling + expect(updateIsEnabled.mock.calls[1]).toEqual([droppable.id, true]); + + // no longer publishing + wrapper.setProps({ + shouldPublish: false, + }); + + // should not do anything + change(); + expect(updateIsEnabled).toHaveBeenCalledTimes(2); + }); + }); }); From c15cae1bfe32bfaabd13ac08f09a193e1c61d133 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 16:05:15 +1000 Subject: [PATCH 096/117] placeholder tests for unnconnected droppable --- test/unit/view/unconnected-droppable.spec.js | 61 +++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/test/unit/view/unconnected-droppable.spec.js b/test/unit/view/unconnected-droppable.spec.js index e2ba96fc7b..4cef5fc464 100644 --- a/test/unit/view/unconnected-droppable.spec.js +++ b/test/unit/view/unconnected-droppable.spec.js @@ -4,6 +4,7 @@ import { mount } from 'enzyme'; // eslint-disable-next-line no-duplicate-imports import type { ReactWrapper } from 'enzyme'; import Droppable from '../../../src/view/droppable/droppable'; +import Placeholder from '../../../src/view/placeholder/'; import { withStore } from '../../utils/get-context-options'; import type { DroppableId } from '../../../src/types'; import type { MapProps, OwnProps, Provided, StateSnapshot } from '../../../src/view/droppable/droppable-types'; @@ -27,9 +28,19 @@ const getStubber = (mock: Function) => const defaultDroppableId: DroppableId = 'droppable-1'; const notDraggingOverMapProps: MapProps = { isDraggingOver: false, + placeholder: null, }; -const isDraggingOverMapProps: MapProps = { +const isDraggingOverHomeMapProps: MapProps = { isDraggingOver: true, + placeholder: null, +}; + +const isDraggingOverForeignMapProps: MapProps = { + isDraggingOver: true, + placeholder: { + width: 100, + height: 50, + }, }; // $ExpectError - not providing children @@ -60,23 +71,55 @@ const mountDroppable = ({ , withStore()); describe('Droppable - unconnected', () => { - it('should provide the props to its children', () => { - const props: MapProps[] = [ - notDraggingOverMapProps, isDraggingOverMapProps, - ]; - - props.forEach((mapProps: MapProps) => { + describe('dragging over home droppable', () => { + it('should provide the props to its children', () => { const myMock = jest.fn(); + mountDroppable({ + mapProps: isDraggingOverHomeMapProps, + WrappedComponent: getStubber(myMock), + }); + const provided: Provided = myMock.mock.calls[0][0].provided; + const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot; + expect(provided.innerRef).toBeInstanceOf(Function); + expect(snapshot.isDraggingOver).toBe(true); + expect(provided.placeholder).toBe(null); + }); + }); + + describe('dragging over foreign droppable', () => { + it('should provide the props to its children', () => { + const myMock = jest.fn(); mountDroppable({ - mapProps, + mapProps: isDraggingOverForeignMapProps, WrappedComponent: getStubber(myMock), }); const provided: Provided = myMock.mock.calls[0][0].provided; const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot; expect(provided.innerRef).toBeInstanceOf(Function); - expect(snapshot.isDraggingOver).toBe(mapProps.isDraggingOver); + expect(snapshot.isDraggingOver).toBe(true); + // $ExpectError - type property of placeholder + expect(provided.placeholder.type).toBe(Placeholder); + // $ExpectError - props property of placeholder + expect(provided.placeholder.props).toEqual(isDraggingOverForeignMapProps.placeholder); + }); + + describe('not dragging over droppable', () => { + it('should provide the props to its children', () => { + const myMock = jest.fn(); + mountDroppable({ + mapProps: notDraggingOverMapProps, + WrappedComponent: getStubber(myMock), + }); + + const provided: Provided = myMock.mock.calls[0][0].provided; + const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot; + expect(provided.innerRef).toBeInstanceOf(Function); + expect(snapshot.isDraggingOver).toBe(false); + expect(provided.placeholder).toBe(null); + }); }); }); }); + From e5a2d4fa15c2b6f7681b6f29c731a31b806dd107 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 16:22:54 +1000 Subject: [PATCH 097/117] adding cross axis move tests to drag handle --- src/view/drag-handle/drag-handle.jsx | 4 +-- test/unit/view/drag-handle.spec.js | 44 ++++++++++++++++------------ 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/view/drag-handle/drag-handle.jsx b/src/view/drag-handle/drag-handle.jsx index d8f6d32a33..edd5430bd4 100644 --- a/src/view/drag-handle/drag-handle.jsx +++ b/src/view/drag-handle/drag-handle.jsx @@ -250,7 +250,7 @@ export default class DragHandle extends Component { this.startPendingMouseDrag(point); }; - executeBasedOnDirection = ({ vertical, horizontal }: Object) => { + executeBasedOnDirection = (fns: ExecuteBasedOnDirection) => { if (!this.props.direction) { console.error('cannot move based on direction when none is provided'); this.stopDragging(() => this.props.callbacks.onCancel()); @@ -258,7 +258,7 @@ export default class DragHandle extends Component { } // eslint-disable-next-line no-unused-expressions - this.props.direction === 'vertical' ? vertical() : horizontal(); + this.props.direction === 'vertical' ? fns.vertical() : fns.horizontal(); } // window keyboard events are bound during a keyboard drag diff --git a/test/unit/view/drag-handle.spec.js b/test/unit/view/drag-handle.spec.js index f067487337..d34f534e0c 100644 --- a/test/unit/view/drag-handle.spec.js +++ b/test/unit/view/drag-handle.spec.js @@ -21,6 +21,8 @@ const getStubCallbacks = (): Callbacks => ({ onMove: jest.fn(), onMoveForward: jest.fn(), onMoveBackward: jest.fn(), + onCrossAxisMoveForward: jest.fn(), + onCrossAxisMoveBackward: jest.fn(), onDrop: jest.fn(), onCancel: jest.fn(), onWindowScroll: jest.fn(), @@ -32,6 +34,8 @@ type CallBacksCalledFn = {| onMove?: number, onMoveForward?: number, onMoveBackward?: number, + onCrossAxisMoveForward ?: number, + onCrossAxisMoveBackward?: number, onDrop?: number, onCancel ?: number, onWindowScroll ?: number, @@ -43,6 +47,8 @@ const callbacksCalled = (callbacks: Callbacks) => ({ onMove = 0, onMoveForward = 0, onMoveBackward = 0, + onCrossAxisMoveForward = 0, + onCrossAxisMoveBackward = 0, onDrop = 0, onCancel = 0, }: CallBacksCalledFn = {}) => @@ -52,7 +58,9 @@ const callbacksCalled = (callbacks: Callbacks) => ({ callbacks.onMoveForward.mock.calls.length === onMoveForward && callbacks.onMoveBackward.mock.calls.length === onMoveBackward && callbacks.onDrop.mock.calls.length === onDrop && - callbacks.onCancel.mock.calls.length === onCancel; + callbacks.onCancel.mock.calls.length === onCancel && + callbacks.onCrossAxisMoveForward.mock.calls.length === onCrossAxisMoveForward && + callbacks.onCrossAxisMoveBackward.mock.calls.length === onCrossAxisMoveBackward; const whereAnyCallbacksCalled = (callbacks: Callbacks) => !callbacksCalled(callbacks)(); @@ -1104,25 +1112,25 @@ describe('drag handle', () => { })).toBe(true); }); - it('should not move backward when the user presses LeftArrow', () => { + it('should request to move to a droppable on the left when the user presses LeftArrow', () => { pressSpacebar(wrapper); windowArrowLeft(); requestAnimationFrame.step(); expect(callbacksCalled(callbacks)({ onKeyLift: 1, - onMoveBackward: 0, + onCrossAxisMoveBackward: 1, })).toBe(true); }); - it('should not move forward when the user presses RightArrow', () => { + it('should request to move to a droppable on the right when the user presses RightArrow', () => { pressSpacebar(wrapper); windowArrowRight(); requestAnimationFrame.step(); expect(callbacksCalled(callbacks)({ onKeyLift: 1, - onMoveForward: 0, + onCrossAxisMoveForward: 1, })).toBe(true); }); }); @@ -1152,49 +1160,47 @@ describe('drag handle', () => { customWrapper.unmount(); }); - it('should not move backward when the user presses ArrowUp', () => { + it('should move backward when the user presses LeftArrow', () => { pressSpacebar(customWrapper); - // try move backward - windowArrowUp(); + windowArrowLeft(); requestAnimationFrame.step(); expect(callbacksCalled(customCallbacks)({ onKeyLift: 1, - onMoveBackward: 0, + onMoveBackward: 1, })).toBe(true); }); - it('should not move forward when the user presses ArrowDown', () => { + it('should move forward when the user presses RightArrow', () => { pressSpacebar(customWrapper); - // try move forward - windowArrowDown(); + windowArrowRight(); requestAnimationFrame.step(); expect(callbacksCalled(customCallbacks)({ onKeyLift: 1, - onMoveForward: 0, + onMoveForward: 1, })).toBe(true); }); - it('should move backward when the user presses LeftArrow', () => { + it('should request a backward cross axis move when the user presses ArrowUp', () => { pressSpacebar(customWrapper); - windowArrowLeft(); + windowArrowUp(); requestAnimationFrame.step(); expect(callbacksCalled(customCallbacks)({ onKeyLift: 1, - onMoveBackward: 1, + onCrossAxisMoveBackward: 1, })).toBe(true); }); - it('should move forward when the user presses RightArrow', () => { + it('should request a forward cross axis move when the user presses ArrowDown', () => { pressSpacebar(customWrapper); - windowArrowRight(); + windowArrowDown(); requestAnimationFrame.step(); expect(callbacksCalled(customCallbacks)({ onKeyLift: 1, - onMoveForward: 1, + onCrossAxisMoveForward: 1, })).toBe(true); }); }); From c7a25cef26cbfc779c1abea4de4b759d58be25ae Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 16:35:51 +1000 Subject: [PATCH 098/117] cross axis move tests for draggable --- test/unit/view/unconnected-draggable.spec.js | 82 +++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/test/unit/view/unconnected-draggable.spec.js b/test/unit/view/unconnected-draggable.spec.js index f151e38204..a06410a927 100644 --- a/test/unit/view/unconnected-draggable.spec.js +++ b/test/unit/view/unconnected-draggable.spec.js @@ -7,7 +7,7 @@ import type { ReactWrapper } from 'enzyme'; import Draggable, { zIndexOptions } from '../../../src/view/draggable/draggable'; import DragHandle, { sloppyClickThreshold } from '../../../src/view/drag-handle/drag-handle'; import Moveable from '../../../src/view/moveable/'; -import Placeholder from '../../../src/view/draggable/placeholder'; +import Placeholder from '../../../src/view/placeholder'; import { css } from '../../../src/view/animation'; import { add, subtract } from '../../../src/state/position'; import type { @@ -77,6 +77,8 @@ const getDispatchPropsStub = (): DispatchProps => ({ moveByWindowScroll: jest.fn(), moveForward: jest.fn(), moveBackward: jest.fn(), + crossAxisMoveForward: jest.fn(), + crossAxisMoveBackward: jest.fn(), drop: jest.fn(), cancel: jest.fn(), dropAnimationFinished: jest.fn(), @@ -712,6 +714,84 @@ describe('Draggable - unconnected', () => { }); }); + describe('onCrossAxisMoveForward', () => { + it('should throw if dragging is disabled', () => { + const wrapper = mountDraggable({ + ownProps: disabledOwnProps, + mapProps: draggingMapProps, + }); + + const tryMove = () => + wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveForward(draggableId); + + expect(tryMove).toThrow(); + }); + + it('should throw if not attached to the DOM', () => { + const wrapper = mountDraggable({ + mapProps: draggingMapProps, + }); + + wrapper.unmount(); + + const tryMove = () => + wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveForward(draggableId); + + expect(tryMove).toThrow(); + }); + + it('should call the cross axis move forward action', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mountDraggable({ + mapProps: draggingMapProps, + dispatchProps, + }); + + wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveForward(draggableId); + + expect(dispatchProps.crossAxisMoveForward).toBeCalledWith(draggableId); + }); + }); + + describe('onCrossAxisMoveBackward', () => { + it('should throw if dragging is disabled', () => { + const wrapper = mountDraggable({ + ownProps: disabledOwnProps, + mapProps: draggingMapProps, + }); + + const tryMove = () => + wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveBackward(draggableId); + + expect(tryMove).toThrow(); + }); + + it('should throw if not attached to the DOM', () => { + const wrapper = mountDraggable({ + mapProps: draggingMapProps, + }); + + wrapper.unmount(); + + const tryMove = () => + wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveBackward(draggableId); + + expect(tryMove).toThrow(); + }); + + it('should call the move cross axis backwards action', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mountDraggable({ + mapProps: draggingMapProps, + dispatchProps, + }); + + wrapper.find(DragHandle).props().callbacks.onCrossAxisMoveBackward(draggableId); + + expect(dispatchProps.crossAxisMoveBackward).toBeCalledWith(draggableId); + }); + }); + describe('onCancel', () => { it('should call the cancel dispatch prop', () => { const dispatchProps = getDispatchPropsStub(); From 95367ff1563be6b2186e0f81a9f05e0c05a243cc Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 17:12:30 +1000 Subject: [PATCH 099/117] fixing flow errors --- src/state/action-creators.js | 10 +++++----- src/state/reducer.js | 4 ++-- .../connected-droppable-dimension-publisher.js | 4 ++-- .../droppable-dimension-publisher-types.js | 14 +++++++++----- test/unit/state/get-drag-impact.spec.js | 1 - test/unit/state/hook-middleware.spec.js | 1 + .../get-best-cross-axis-droppable.spec.js | 2 +- test/utils/get-droppable-with-draggables.js | 7 ++++--- 8 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/state/action-creators.js b/src/state/action-creators.js index b69ebcfe09..a7f6bc4f03 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -133,17 +133,17 @@ export const updateDroppableDimensionScroll = }, }); -export type UpdateDroppableIsEnabledAction = {| - type: 'UPDATE_DROPPABLE_IS_ENABLED', +export type UpdateDroppableDimensionIsEnabledAction = {| + type: 'UPDATE_DROPPABLE_DIMENSION_IS_ENABLED', payload: { id: DroppableId, isEnabled: boolean, } |} -export const updateDroppableIsEnabled = - (id: DroppableId, isEnabled: boolean): UpdateDroppableIsEnabledAction => ({ - type: 'UPDATE_DROPPABLE_IS_ENABLED', +export const updateDroppableDimensionIsEnabled = + (id: DroppableId, isEnabled: boolean): UpdateDroppableDimensionIsEnabledAction => ({ + type: 'UPDATE_DROPPABLE_DIMENSION_IS_ENABLED', payload: { id, isEnabled, diff --git a/src/state/reducer.js b/src/state/reducer.js index 15f3ca3a43..145c9988ba 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -24,7 +24,7 @@ import { add, subtract, negate } from './position'; import getDragImpact from './get-drag-impact'; import moveToNextIndex from './move-to-next-index/'; import type { Result as MoveToNextResult } from './move-to-next-index/move-to-next-index-types'; -import type { Result as MoveCrossAxisResult } from './move-cross-axis/move-to-new-droppable/move-to-new-droppable-types'; +import type { Result as MoveCrossAxisResult } from './move-cross-axis/move-cross-axis-types'; import moveCrossAxis from './move-cross-axis/'; const noDimensions: DimensionState = { @@ -349,7 +349,7 @@ export default (state: State = clean('IDLE'), action: Action): State => { }); } - if (action.type === 'UPDATE_DROPPABLE_IS_ENABLED') { + if (action.type === 'UPDATE_DROPPABLE_DIMENSION_IS_ENABLED') { if (!Object.keys(state.dimension.droppable).length) { return state; } diff --git a/src/view/droppable-dimension-publisher/connected-droppable-dimension-publisher.js b/src/view/droppable-dimension-publisher/connected-droppable-dimension-publisher.js index 1389641bbe..dc69804947 100644 --- a/src/view/droppable-dimension-publisher/connected-droppable-dimension-publisher.js +++ b/src/view/droppable-dimension-publisher/connected-droppable-dimension-publisher.js @@ -13,8 +13,8 @@ import { storeKey } from '../context-keys'; import DroppableDimensionPublisher from './droppable-dimension-publisher'; import { publishDroppableDimension, - updateDroppableIsEnabled, updateDroppableDimensionScroll, + updateDroppableDimensionIsEnabled, } from '../../state/action-creators'; const requestDimensionSelector = @@ -44,7 +44,7 @@ const makeMapStateToProps = () => { const mapDispatchToProps: DispatchProps = { publish: publishDroppableDimension, updateScroll: updateDroppableDimensionScroll, - updateIsEnabled: updateDroppableIsEnabled, + updateIsEnabled: updateDroppableDimensionIsEnabled, }; export default connect( diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js b/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js index f9012f3d33..b48c0e1704 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher-types.js @@ -1,11 +1,15 @@ // @flow +import type { PropType } from 'babel-plugin-react-flow-props-to-prop-types'; +import { + publishDroppableDimension, + updateDroppableDimensionIsEnabled, + updateDroppableDimensionScroll, +} from '../../state/action-creators'; import type { - DroppableDimension, DroppableId, TypeId, ReactElement, HTMLElement, - Position, Direction, } from '../../types'; @@ -14,9 +18,9 @@ export type MapProps = {| |} export type DispatchProps = {| - publish: (dimension: DroppableDimension) => void, - updateIsEnabled: (id: DroppableId, isEnabled: boolean) => void, - updateScroll: (id: DroppableId, offset: Position) => void, + publish: PropType, + updateIsEnabled: PropType, + updateScroll: PropType, |} export type OwnProps = {| diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index dcade01432..492dd81633 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -1136,7 +1136,6 @@ describe('get drag impact', () => { describe('moving between lists', () => { const homeDroppable = getDroppableWithDraggables({ - direction: 'vertical', droppableId: 'drop-home', droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, draggableRects: [ diff --git a/test/unit/state/hook-middleware.spec.js b/test/unit/state/hook-middleware.spec.js index a04dc4e2d8..b442facead 100644 --- a/test/unit/state/hook-middleware.spec.js +++ b/test/unit/state/hook-middleware.spec.js @@ -124,6 +124,7 @@ const state = (() => { center: currentClient.center, }, shouldAnimate: true, + isScrollAllowed: true, }, impact: noImpact, }; 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 2e3358aa5a..85ebd1c733 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 @@ -1,5 +1,5 @@ // @flow -import getBestCrossAxisDroppable from '../../../../src/state/move-to-best-droppable/get-best-cross-axis-droppable'; +import getBestCrossAxisDroppable from '../../../../src/state/move-cross-axis/get-best-cross-axis-droppable'; import { getDroppableDimension } from '../../../../src/state/dimension'; import getClientRect from '../../../utils/get-client-rect'; import { add } from '../../../../src/state/position'; diff --git a/test/utils/get-droppable-with-draggables.js b/test/utils/get-droppable-with-draggables.js index a6616f7a96..58d6ce4ee1 100644 --- a/test/utils/get-droppable-with-draggables.js +++ b/test/utils/get-droppable-with-draggables.js @@ -1,6 +1,5 @@ // @flow import { getDraggableDimension, getDroppableDimension } from '../../src/state/dimension'; -import type { ClientRect } from '../../src/state/dimension'; import type { DroppableId, DraggableDimension, @@ -19,7 +18,7 @@ type ClientRectSubset = {| type Args = {| direction?: Direction, - droppableId: DroppableId, + droppableId?: DroppableId, droppableRect: ClientRectSubset, draggableRects: ClientRectSubset[], |}; @@ -32,9 +31,11 @@ export type Result = { draggableDimensions: DraggableDimension[], }; +let idCount = 0; + export default ({ direction = 'vertical', - droppableId, + droppableId = `droppable-generated-id-${idCount++}`, droppableRect, draggableRects, }: Args): Result => { From 18762e0374a417760aeddb6991187992daa2e518 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 20:34:08 +1000 Subject: [PATCH 100/117] initial impact now handled independently --- src/state/get-drag-impact.js | 47 +++---------- src/state/get-initial-impact.js | 47 +++++++++++++ src/state/reducer.js | 32 +++++---- src/view/droppable/connected-droppable.js | 2 + stories/src/multiple-vertical/quote-app.jsx | 5 +- test/unit/state/get-initial-impact.spec.js | 76 +++++++++++++++++++++ 6 files changed, 156 insertions(+), 53 deletions(-) create mode 100644 src/state/get-initial-impact.js create mode 100644 test/unit/state/get-initial-impact.spec.js diff --git a/src/state/get-drag-impact.js b/src/state/get-drag-impact.js index 36505fe350..a4970c3209 100644 --- a/src/state/get-drag-impact.js +++ b/src/state/get-drag-impact.js @@ -66,56 +66,29 @@ export default ({ return noImpact; } - const newCenter = withinDroppable.center; - const draggingDimension: DraggableDimension = draggables[draggableId]; const droppable: DroppableDimension = droppables[droppableId]; const axis: Axis = droppable.axis; - const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( - droppable, - draggables, - ); - - // If the droppable is disabled we still need to return an impact with a destination, otherwise - // we'll get errors when trying to lift from a disabled droppable (which is allowed) if (!droppable.isEnabled) { - const homeDroppableId = draggingDimension.droppableId; - const homeIndex = getDraggablesInsideDroppable( - droppables[homeDroppableId], - draggables, - ).indexOf(draggingDimension); return { movement: noMovement, - direction: null, - destination: { - droppableId: homeDroppableId, - index: homeIndex, - }, + direction: axis.direction, + destination: null, }; } - // If the droppable is disabled we still need to return an impact with a destination, otherwise - // we'll get errors when trying to lift from a disabled droppable (which is allowed) - if (!droppable.isEnabled) { - const homeDroppableId = draggingDimension.droppableId; - const homeIndex = getDraggablesInsideDroppable( - droppables[homeDroppableId], - draggables, - ).indexOf(draggingDimension); - return { - movement: noMovement, - direction: null, - destination: { - droppableId: homeDroppableId, - index: homeIndex, - }, - }; - } + const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( + droppable, + draggables, + ); + + const newCenter: Position = withinDroppable.center; + const draggingDimension: DraggableDimension = draggables[draggableId]; + const isWithinHomeDroppable = draggingDimension.droppableId === droppableId; // not considering margin so that items move based on visible edges const draggableCenter: Position = draggingDimension.page.withoutMargin.center; const isBeyondStartPosition: boolean = newCenter[axis.line] - draggableCenter[axis.line] > 0; - const isWithinHomeDroppable = draggingDimension.droppableId === droppableId; const shouldDisplaceItemsForward = isWithinHomeDroppable ? isBeyondStartPosition : false; const moved: DraggableId[] = insideDroppable diff --git a/src/state/get-initial-impact.js b/src/state/get-initial-impact.js new file mode 100644 index 0000000000..77b0317069 --- /dev/null +++ b/src/state/get-initial-impact.js @@ -0,0 +1,47 @@ +// @flow +import getDraggablesInsideDroppable from './get-draggables-inside-droppable'; +import { noMovement } from './no-impact'; +import type { + DraggableLocation, + DraggableDimension, + DraggableDimensionMap, + DroppableDimension, + DragImpact, +} from '../types'; + +type Args = {| + draggable: DraggableDimension, + droppable: DroppableDimension, + draggables: DraggableDimensionMap +|} + +export default ({ + draggable, + droppable, + draggables, +}: Args): ?DragImpact => { + const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( + droppable, + draggables, + ); + + const homeIndex: number = insideDroppable.indexOf(draggable); + + if (homeIndex === -1) { + console.error('lifting a draggable that is not inside a droppable'); + return null; + } + + const home: DraggableLocation = { + index: homeIndex, + droppableId: droppable.id, + }; + + const impact: DragImpact = { + movement: noMovement, + direction: droppable.axis.direction, + destination: home, + }; + + return impact; +}; diff --git a/src/state/reducer.js b/src/state/reducer.js index 145c9988ba..24fc304779 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -5,6 +5,7 @@ import type { TypeId, State, DraggableDimension, DroppableDimension, + DraggableDimensionMap, DroppableId, DraggableId, DimensionState, @@ -20,6 +21,7 @@ import type { TypeId, Position, WithinDroppable, } from '../types'; +import getInitialImpact from './get-initial-impact'; import { add, subtract, negate } from './position'; import getDragImpact from './get-drag-impact'; import moveToNextIndex from './move-to-next-index/'; @@ -230,27 +232,27 @@ export default (state: State = clean('IDLE'), action: Action): State => { } const { id, type, client, page, windowScroll, isScrollAllowed } = action.payload; + const draggables: DraggableDimensionMap = state.dimension.draggable; + const draggable: DraggableDimension = state.dimension.draggable[id]; + const droppable: DroppableDimension = state.dimension.droppable[draggable.droppableId]; - // no scroll diff yet so withinDroppable is just the center position - const withinDroppable: WithinDroppable = { - center: page.center, - }; - - const impact: DragImpact = getDragImpact({ - page: page.selection, - withinDroppable, - draggableId: id, - draggables: state.dimension.draggable, - droppables: state.dimension.droppable, + const impact: ?DragImpact = getInitialImpact({ + draggable, + droppable, + draggables, }); - const source: ?DraggableLocation = impact.destination; - - if (!source) { - console.error('lifting a draggable that is not inside a droppable'); + if (!impact || !impact.destination) { + console.error('invalid lift state'); return clean(); } + const source: DraggableLocation = impact.destination; + + const withinDroppable: WithinDroppable = { + center: page.center, + }; + const initial: InitialDrag = { source, client, diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index a98544eb8c..0db8118da8 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -34,6 +34,7 @@ export const makeSelector = (): Selector => { const getIsDraggingOver = memoizeOne( (id: DroppableId, destination: ?DraggableLocation): boolean => { + console.log('destination', destination); if (!destination) { return false; } @@ -110,6 +111,7 @@ export const makeSelector = (): Selector => { } const isDraggingOver = getIsDraggingOver(id, drag.impact.destination); + const placeholder: ?Placeholder = getPlaceholder( id, drag.initial.source, diff --git a/stories/src/multiple-vertical/quote-app.jsx b/stories/src/multiple-vertical/quote-app.jsx index 57792d2caa..1ea6ac60c1 100644 --- a/stories/src/multiple-vertical/quote-app.jsx +++ b/stories/src/multiple-vertical/quote-app.jsx @@ -132,9 +132,12 @@ export default class QuoteApp extends Component { if (!sourceDroppable) { return null; } + return null; + const droppables: string[] = ['alpha', 'beta', 'gamma', 'delta']; const sourceIndex = droppables.indexOf(sourceDroppable); - const disabledDroppableIndex = (sourceIndex + 1) % droppables.length; + // const disabledDroppableIndex = (sourceIndex + 1) % droppables.length; + const disabledDroppableIndex = sourceIndex; return droppables[disabledDroppableIndex]; } diff --git a/test/unit/state/get-initial-impact.spec.js b/test/unit/state/get-initial-impact.spec.js new file mode 100644 index 0000000000..0ab8a08785 --- /dev/null +++ b/test/unit/state/get-initial-impact.spec.js @@ -0,0 +1,76 @@ +// @flow +import getInitialImpact from '../../../src/state/get-initial-impact'; +import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; +import { getDraggableDimension } from '../../../src/state/dimension'; +import getClientRect from '../../utils/get-client-rect'; +import { noMovement } from '../../../src/state/no-impact'; +import type { Result as Data } from '../../utils/get-droppable-with-draggables'; +import type { + DraggableDimensionMap, + DraggableDimension, + DroppableDimension, + DragImpact, +} from '../../../src/types'; + +const data: Data = getDroppableWithDraggables({ + droppableId: 'droppable', + droppableRect: { top: 0, left: 0, right: 100, bottom: 100 }, + draggableRects: [ + { top: 0, left: 0, right: 100, bottom: 20 }, + { top: 20, left: 0, right: 100, bottom: 40 }, + { top: 40, left: 0, right: 100, bottom: 60 }, + { top: 60, left: 0, right: 100, bottom: 80 }, + ], +}); + +const droppable: DroppableDimension = data.droppable; +const draggables: DraggableDimensionMap = data.draggables; + +describe('get initial impact', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { }); + }); + + afterEach(() => { + console.error.mockRestore(); + }); + + it('should return null if the draggable cannot be found in the droppable', () => { + const mystery = getDraggableDimension({ + id: 'mystery', + droppableId: 'some-cool-id', + clientRect: getClientRect({ + top: 0, left: 100, right: 200, bottom: 20, + }), + }); + + const result: ?DragImpact = getInitialImpact({ + draggable: mystery, + droppable: data.droppable, + draggables: data.draggables, + }); + + expect(result).toBe(null); + expect(console.error).toHaveBeenCalled(); + }); + + it('should return the initial location', () => { + data.draggableDimensions.forEach((draggable: DraggableDimension, index: number) => { + const expected: DragImpact = { + movement: noMovement, + direction: droppable.axis.direction, + destination: { + droppableId: droppable.id, + index, + }, + }; + const impact: ?DragImpact = getInitialImpact({ + draggable, + droppable, + draggables, + }); + + expect(impact).toEqual(expected); + }); + }); +}); From e1a6fee4575ded3e3322754ca1d21d8b19c0aacd Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Tue, 12 Sep 2017 20:52:46 +1000 Subject: [PATCH 101/117] adding tests for impact for disabled droppables --- test/unit/state/get-drag-impact.spec.js | 83 +++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index 492dd81633..8001e6443e 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -9,6 +9,7 @@ import noImpact from '../../../src/state/no-impact'; import getClientRect from '../../utils/get-client-rect'; import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; import { add, patch } from '../../../src/state/position'; +import { noMovement } from '../../../src/state/no-impact'; import type { WithinDroppable, DroppableId, @@ -85,7 +86,7 @@ describe('get drag impact', () => { }; it('should return no movement when not dragging over anything', () => { - // dragging up above the list + // dragging up above the list const page: Position = { x: droppable.page.withMargin.left, y: droppable.page.withMargin.top - 100, @@ -109,7 +110,7 @@ describe('get drag impact', () => { describe('moving forward', () => { describe('not moved far enough', () => { it('should return the starting position', () => { - // moving forward - but not enough + // moving forward - but not enough const page: Position = { x: draggable2.page.withoutMargin.center.x, y: draggable2.page.withoutMargin.center.y + 1, @@ -143,7 +144,7 @@ describe('get drag impact', () => { }); describe('moving past one item', () => { - // moving forward past the top of the next item + // moving forward past the top of the next item const page: Position = { x: draggable1.page.withoutMargin.center.x, y: draggable2.page.withoutMargin.top + 1, @@ -192,7 +193,7 @@ describe('get drag impact', () => { }); describe('moving past two items', () => { - // moving forward past the top of the third item + // moving forward past the top of the third item const page: Position = { x: draggable1.page.withoutMargin.center.x, y: draggable3.page.withoutMargin.top + 1, @@ -241,7 +242,7 @@ describe('get drag impact', () => { }); describe('moving past one item when the dragging item is not the first in the list', () => { - // moving the second item forward past the top of the third item + // moving the second item forward past the top of the third item const page: Position = { x: draggable2.page.withoutMargin.center.x, y: draggable3.page.withMargin.top + 1, @@ -290,7 +291,7 @@ describe('get drag impact', () => { }); describe('moving past an item due to change in droppable scroll', () => { - // using the center position of the draggable as the selection point + // using the center position of the draggable as the selection point const page: Position = draggable1.page.withMargin.center; const withinDroppable: WithinDroppable = { // just over the top of the second item @@ -576,6 +577,41 @@ describe('get drag impact', () => { }); }); }); + + describe('moving over disabled list', () => { + it('should return an empty impact', () => { + // moving forward past the top of the next item + const page: Position = { + x: draggable1.page.withoutMargin.center.x, + y: draggable2.page.withoutMargin.top + 1, + }; + const withinDroppable: WithinDroppable = { + center: page, + }; + const disabled = { + ...droppable, + isEnabled: false, + }; + const custom: DraggableDimensionMap = { + [disabled.id]: disabled, + }; + const expected: DragImpact = { + movement: noMovement, + direction: droppable.axis.direction, + destination: null, + }; + + const impact: DragImpact = getDragImpact({ + page, + withinDroppable, + draggableId: draggable1.id, + draggables, + droppables: custom, + }); + + expect(impact).toEqual(expected); + }); + }); }); // same tests as vertical - but moving on the horizontal plane @@ -1132,6 +1168,41 @@ describe('get drag impact', () => { }); }); }); + + describe('moving over disabled list', () => { + it('should return an empty impact', () => { + // moving forward past the right of the next item + const page: Position = { + x: draggable2.page.withoutMargin.left + 1, + y: draggable1.page.withoutMargin.center.y, + }; + const withinDroppable: WithinDroppable = { + center: page, + }; + const disabled = { + ...droppable, + isEnabled: false, + }; + const custom: DraggableDimensionMap = { + [disabled.id]: disabled, + }; + const expected: DragImpact = { + movement: noMovement, + direction: droppable.axis.direction, + destination: null, + }; + + const impact: DragImpact = getDragImpact({ + page, + withinDroppable, + draggableId: draggable1.id, + draggables, + droppables: custom, + }); + + expect(impact).toEqual(expected); + }); + }); }); describe('moving between lists', () => { From d54cf87759805978c2d1e6be082088b916aef871 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 13 Sep 2017 08:06:38 +1000 Subject: [PATCH 102/117] more robust dimension clipping --- .../droppable-dimension-publisher.jsx | 39 +++++++++---------- src/view/droppable/connected-droppable.js | 1 - ...cted-droppable-dimension-publisher.spec.js | 8 +++- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 088d02fa6f..951cf8ebce 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -11,6 +11,9 @@ import type { Margin, ClientRect } from '../../state/dimension'; import type { DroppableDimension, Position, HTMLElement } from '../../types'; import type { Props } from './droppable-dimension-publisher-types'; +// TEMP - move to source if needed +import getClientRect from '../../../test/utils/get-client-rect'; + const origin: Position = { x: 0, y: 0 }; export default class DroppableDimensionPublisher extends Component { @@ -37,6 +40,7 @@ export default class DroppableDimensionPublisher extends Component { const { droppableId, direction, isDropDisabled, targetRef } = this.props; invariant(targetRef, 'DimensionPublisher cannot calculate a dimension when not attached to the DOM'); + const scroll: Position = this.getScrollOffset(); const style = window.getComputedStyle(targetRef); const margin: Margin = { @@ -57,28 +61,23 @@ export default class DroppableDimensionPublisher extends Component { return current; } - // Clipping the droppables dimensions by its scroll parent. - // If the scroll parent is smaller in size than the droppable we need to trim the - // dimensions of the droppable so that it is has the correct visible coordinates - // Still using the margin of the droppable. We can revisit the margin selection decision + // We need to trim the dimension by the visible area of the scroll container - const parent: ClientRect = this.closestScrollable.getBoundingClientRect(); + // Adjust the current dimension with the parents scroll + const top = current.top + scroll.y; + const bottom = current.bottom + scroll.y; + const left = current.left + scroll.x; + const right = current.right + scroll.x; - const top = Math.max(current.top, parent.top); - const left = Math.max(current.left, parent.left); - const right = Math.min(current.right, parent.right); - const bottom = Math.min(current.bottom, parent.bottom); - - const result: ClientRect = { - top, - right, - bottom, - left, - width: (right - left), - height: (bottom - top), - }; - - return result; + // Trim the dimension by the size of the parent + + const parent: ClientRect = this.closestScrollable.getBoundingClientRect(); + return getClientRect({ + top: Math.max(top, parent.top), + left: Math.max(left, parent.left), + right: Math.min(right, parent.right), + bottom: Math.min(bottom, parent.bottom), + }); })(); const dimension: DroppableDimension = getDroppableDimension({ diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index 0db8118da8..dd9367401e 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -34,7 +34,6 @@ export const makeSelector = (): Selector => { const getIsDraggingOver = memoizeOne( (id: DroppableId, destination: ?DraggableLocation): boolean => { - console.log('destination', destination); if (!destination) { return false; } diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js index de7bdd2d4e..b5d07965f4 100644 --- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js @@ -406,7 +406,13 @@ describe('DraggableDimensionPublisher', () => { }); const expected = getDroppableDimension({ id: droppableId, - clientRect: scrollParentRect, + clientRect: getClientRect({ + top: 0, + bottom: 100, + left: 0, + // using max value... + right: 110, + }), }); (() => { let count = 0; From 0e96515d603f290ec6954873a684eb8b6db4a5a6 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 13 Sep 2017 08:37:25 +1000 Subject: [PATCH 103/117] fixing flow errors. moving get-client-rect to src --- src/state/get-client-rect.js | 19 +++++++++++++++++++ .../droppable-dimension-publisher.jsx | 4 +--- .../integration/hooks-integration.spec.js | 2 +- test/unit/state/get-drag-impact.spec.js | 12 +++++++----- .../get-draggables-inside-droppable.spec.js | 2 +- test/unit/state/get-droppable-over.spec.js | 2 +- test/unit/state/get-initial-impact.spec.js | 2 +- test/unit/state/hook-middleware.spec.js | 2 +- ...within-visible-bounds-of-droppable.spec.js | 2 +- .../get-best-cross-axis-droppable.spec.js | 2 +- .../get-closest-draggable.spec.js | 2 +- .../move-to-new-droppable.spec.js | 2 +- test/unit/state/move-to-edge.spec.js | 2 +- test/unit/state/move-to-next-index.spec.js | 2 +- test/unit/view/connected-draggable.spec.js | 2 +- test/unit/view/connected-droppable.spec.js | 2 +- ...cted-draggable-dimension-publisher.spec.js | 2 +- test/unit/view/unconnected-draggable.spec.js | 2 +- ...cted-droppable-dimension-publisher.spec.js | 2 +- test/utils/get-client-rect.js | 18 ------------------ test/utils/get-droppable-with-draggables.js | 2 +- 21 files changed, 44 insertions(+), 43 deletions(-) create mode 100644 src/state/get-client-rect.js delete mode 100644 test/utils/get-client-rect.js diff --git a/src/state/get-client-rect.js b/src/state/get-client-rect.js new file mode 100644 index 0000000000..ecf212339f --- /dev/null +++ b/src/state/get-client-rect.js @@ -0,0 +1,19 @@ +// @flow +import type { ClientRect } from './dimension'; + +type Args = { + top: number, + right: number, + bottom: number, + left: number, +} + +// $ExpectError - flow cannot handle using an axis to create these +export default ({ top, right, bottom, left }: Args): ClientRect => ({ + top, + right, + bottom, + left, + width: (right - left), + height: (bottom - top), +}); diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 951cf8ebce..ef1cadce0d 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -4,6 +4,7 @@ import invariant from 'invariant'; import rafScheduler from 'raf-schd'; import memoizeOne from 'memoize-one'; import getWindowScrollPosition from '../get-window-scroll-position'; +import getClientRect from '../../state/get-client-rect'; import { getDroppableDimension } from '../../state/dimension'; import getClosestScrollable from '../get-closest-scrollable'; // eslint-disable-next-line no-duplicate-imports @@ -11,9 +12,6 @@ import type { Margin, ClientRect } from '../../state/dimension'; import type { DroppableDimension, Position, HTMLElement } from '../../types'; import type { Props } from './droppable-dimension-publisher-types'; -// TEMP - move to source if needed -import getClientRect from '../../../test/utils/get-client-rect'; - const origin: Position = { x: 0, y: 0 }; export default class DroppableDimensionPublisher extends Component { diff --git a/test/unit/integration/hooks-integration.spec.js b/test/unit/integration/hooks-integration.spec.js index b16263becb..1d26b0d9e0 100644 --- a/test/unit/integration/hooks-integration.spec.js +++ b/test/unit/integration/hooks-integration.spec.js @@ -4,7 +4,7 @@ import { mount } from 'enzyme'; import { DragDropContext, Draggable, Droppable } from '../../../src/'; import { sloppyClickThreshold } from '../../../src/view/drag-handle/drag-handle'; import { dispatchWindowMouseEvent, dispatchWindowKeyDownEvent, mouseEvent } from '../../utils/user-input-util'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import type { Hooks, DraggableLocation, diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index 8001e6443e..585c442cb5 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -6,7 +6,7 @@ import { // eslint-disable-next-line no-duplicate-imports import getDragImpact from '../../../src/state/get-drag-impact'; import noImpact from '../../../src/state/no-impact'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; import { add, patch } from '../../../src/state/position'; import { noMovement } from '../../../src/state/no-impact'; @@ -588,11 +588,12 @@ describe('get drag impact', () => { const withinDroppable: WithinDroppable = { center: page, }; - const disabled = { + // $ExpectError - using spread + const disabled: DroppableDimension = { ...droppable, isEnabled: false, }; - const custom: DraggableDimensionMap = { + const custom: DroppableDimensionMap = { [disabled.id]: disabled, }; const expected: DragImpact = { @@ -1179,11 +1180,12 @@ describe('get drag impact', () => { const withinDroppable: WithinDroppable = { center: page, }; - const disabled = { + // $ExpectError - using flow + const disabled: DroppableDimension = { ...droppable, isEnabled: false, }; - const custom: DraggableDimensionMap = { + const custom: DroppableDimensionMap = { [disabled.id]: disabled, }; const expected: DragImpact = { diff --git a/test/unit/state/get-draggables-inside-droppable.spec.js b/test/unit/state/get-draggables-inside-droppable.spec.js index ecc9e60d06..27e5ea840f 100644 --- a/test/unit/state/get-draggables-inside-droppable.spec.js +++ b/test/unit/state/get-draggables-inside-droppable.spec.js @@ -1,7 +1,7 @@ // @flow import getDraggablesInsideDroppable from '../../../src/state/get-draggables-inside-droppable'; import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import type { DroppableId, DraggableDimension, diff --git a/test/unit/state/get-droppable-over.spec.js b/test/unit/state/get-droppable-over.spec.js index eecd40c29c..13703c7a0c 100644 --- a/test/unit/state/get-droppable-over.spec.js +++ b/test/unit/state/get-droppable-over.spec.js @@ -1,7 +1,7 @@ // @flow import getDroppableOver from '../../../src/state/get-droppable-over'; import { getDroppableDimension } from '../../../src/state/dimension'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import type { DroppableDimension, DroppableDimensionMap, diff --git a/test/unit/state/get-initial-impact.spec.js b/test/unit/state/get-initial-impact.spec.js index 0ab8a08785..77cdca666b 100644 --- a/test/unit/state/get-initial-impact.spec.js +++ b/test/unit/state/get-initial-impact.spec.js @@ -2,7 +2,7 @@ import getInitialImpact from '../../../src/state/get-initial-impact'; import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; import { getDraggableDimension } from '../../../src/state/dimension'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import { noMovement } from '../../../src/state/no-impact'; import type { Result as Data } from '../../utils/get-droppable-with-draggables'; import type { diff --git a/test/unit/state/hook-middleware.spec.js b/test/unit/state/hook-middleware.spec.js index b442facead..42a600d1d0 100644 --- a/test/unit/state/hook-middleware.spec.js +++ b/test/unit/state/hook-middleware.spec.js @@ -2,7 +2,7 @@ import middleware from '../../../src/state/hook-middleware'; import { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; import { clean } from '../../../src/state/action-creators'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import noImpact from '../../../src/state/no-impact'; import type { DraggableId, diff --git a/test/unit/state/is-within-visible-bounds-of-droppable.spec.js b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js index 4739f3bd7b..43826498e8 100644 --- a/test/unit/state/is-within-visible-bounds-of-droppable.spec.js +++ b/test/unit/state/is-within-visible-bounds-of-droppable.spec.js @@ -2,7 +2,7 @@ import { isPointWithin, isDraggableWithin } from '../../../src/state/is-within-visible-bounds-of-droppable'; import { getDroppableDimension } from '../../../src/state/dimension'; import { add, subtract } from '../../../src/state/position'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; import type { Result } from '../../utils/get-droppable-with-draggables'; import type { 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 85ebd1c733..ee212c83f8 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 @@ -1,7 +1,7 @@ // @flow import getBestCrossAxisDroppable from '../../../../src/state/move-cross-axis/get-best-cross-axis-droppable'; import { getDroppableDimension } from '../../../../src/state/dimension'; -import getClientRect from '../../../utils/get-client-rect'; +import getClientRect from '../../../../src/state/get-client-rect'; import { add } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; import type { 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 0ac6e23772..676b2573dd 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 @@ -3,7 +3,7 @@ import getClosestDraggable from '../../../../src/state/move-cross-axis/get-close import { getDroppableDimension, getDraggableDimension } from '../../../../src/state/dimension'; import { add, distance, patch } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; -import getClientRect from '../../../utils/get-client-rect'; +import getClientRect from '../../../../src/state/get-client-rect'; import type { Axis, Position, 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 43492d1212..54ba6020a3 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 @@ -2,7 +2,7 @@ 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 getClientRect from '../../../utils/get-client-rect'; +import getClientRect from '../../../../src/state/get-client-rect'; import moveToEdge from '../../../../src/state/move-to-edge'; import { patch } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; diff --git a/test/unit/state/move-to-edge.spec.js b/test/unit/state/move-to-edge.spec.js index b1dc2ae7fa..10ed9fc01a 100644 --- a/test/unit/state/move-to-edge.spec.js +++ b/test/unit/state/move-to-edge.spec.js @@ -7,7 +7,7 @@ import { subtract, } from '../../../src/state/position'; import getFragment from '../../utils/get-fragment'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import moveToEdge from '../../../src/state/move-to-edge'; import { vertical, horizontal } from '../../../src/state/axis'; import type { diff --git a/test/unit/state/move-to-next-index.spec.js b/test/unit/state/move-to-next-index.spec.js index d94a27079e..a268a7aa01 100644 --- a/test/unit/state/move-to-next-index.spec.js +++ b/test/unit/state/move-to-next-index.spec.js @@ -2,7 +2,7 @@ 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 { getDraggableDimension, getDroppableDimension } from '../../../src/state/dimension'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import moveToEdge from '../../../src/state/move-to-edge'; import { patch } from '../../../src/state/position'; import { vertical, horizontal } from '../../../src/state/axis'; diff --git a/test/unit/view/connected-draggable.spec.js b/test/unit/view/connected-draggable.spec.js index 1f327ff20a..2981470ce6 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 { getDraggableDimension } from '../../../src/state/dimension'; import noImpact from '../../../src/state/no-impact'; import { combine, withStore, withDroppableId } from '../../utils/get-context-options'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import { add } from '../../../src/state/position'; import type { CurrentDrag, diff --git a/test/unit/view/connected-droppable.spec.js b/test/unit/view/connected-droppable.spec.js index 6ade9030f0..6d2c6f899b 100644 --- a/test/unit/view/connected-droppable.spec.js +++ b/test/unit/view/connected-droppable.spec.js @@ -6,7 +6,7 @@ import Droppable, { makeSelector } from '../../../src/view/droppable/connected-d import noImpact from '../../../src/state/no-impact'; import { getDraggableDimension } from '../../../src/state/dimension'; import { withStore } from '../../utils/get-context-options'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import type { Phase, DragState, diff --git a/test/unit/view/unconnected-draggable-dimension-publisher.spec.js b/test/unit/view/unconnected-draggable-dimension-publisher.spec.js index e5ba43f1b5..4b972fcdf5 100644 --- a/test/unit/view/unconnected-draggable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-draggable-dimension-publisher.spec.js @@ -5,7 +5,7 @@ import DraggableDimensionPublisher from '../../../src/view/draggable-dimension-p import { getDraggableDimension } from '../../../src/state/dimension'; // eslint-disable-next-line no-duplicate-imports import type { ClientRect, Margin } from '../../../src/state/dimension'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import setWindowScroll from '../../utils/set-window-scroll'; import type { Position, diff --git a/test/unit/view/unconnected-draggable.spec.js b/test/unit/view/unconnected-draggable.spec.js index a06410a927..8372719222 100644 --- a/test/unit/view/unconnected-draggable.spec.js +++ b/test/unit/view/unconnected-draggable.spec.js @@ -28,7 +28,7 @@ import type { InitialDragLocation, } from '../../../src/types'; import { getDraggableDimension } from '../../../src/state/dimension'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import { combine, withStore, withDroppableId } from '../../utils/get-context-options'; import { dispatchWindowMouseEvent, mouseEvent } from '../../utils/user-input-util'; import setWindowScroll from '../../utils/set-window-scroll'; diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js index b5d07965f4..d486d02387 100644 --- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js @@ -6,7 +6,7 @@ import DroppableDimensionPublisher from '../../../src/view/droppable-dimension-p import { getDroppableDimension } from '../../../src/state/dimension'; // eslint-disable-next-line no-duplicate-imports import type { Margin, ClientRect } from '../../../src/state/dimension'; -import getClientRect from '../../utils/get-client-rect'; +import getClientRect from '../../../src/state/get-client-rect'; import setWindowScroll from '../../utils/set-window-scroll'; import type { DroppableId, diff --git a/test/utils/get-client-rect.js b/test/utils/get-client-rect.js deleted file mode 100644 index 80daefb764..0000000000 --- a/test/utils/get-client-rect.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow -import type { ClientRect } from '../../src/state/dimension'; - -type GetClientRect = {| - top: number, - right: number, - bottom: number, - left: number, -|} - -export default ({ top, right, bottom, left }: GetClientRect): ClientRect => ({ - top, - right, - bottom, - left, - width: (right - left), - height: (bottom - top), -}); diff --git a/test/utils/get-droppable-with-draggables.js b/test/utils/get-droppable-with-draggables.js index 58d6ce4ee1..7096cf20e2 100644 --- a/test/utils/get-droppable-with-draggables.js +++ b/test/utils/get-droppable-with-draggables.js @@ -7,7 +7,7 @@ import type { DroppableDimension, Direction, } from '../../src/types'; -import getClientRect from './get-client-rect'; +import getClientRect from '../../src/state/get-client-rect'; type ClientRectSubset = {| top: number, From d46d3301e065d1ec9c43265bc3a7f1d48ff32ed3 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 13 Sep 2017 11:11:31 +1000 Subject: [PATCH 104/117] adding tests for droppable dimension clipping --- .../droppable-dimension-publisher.jsx | 4 +- ...cted-droppable-dimension-publisher.spec.js | 306 +++++++++++++++--- 2 files changed, 256 insertions(+), 54 deletions(-) diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index ef1cadce0d..13e7ba88b3 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -41,6 +41,8 @@ export default class DroppableDimensionPublisher extends Component { const scroll: Position = this.getScrollOffset(); const style = window.getComputedStyle(targetRef); + // keeping it simple and always using the margin of the droppable + const margin: Margin = { top: parseInt(style.marginTop, 10), right: parseInt(style.marginRight, 10), @@ -84,7 +86,7 @@ export default class DroppableDimensionPublisher extends Component { clientRect, margin, windowScroll: getWindowScrollPosition(), - scroll: this.getScrollOffset(), + scroll, isEnabled: !isDropDisabled, }); diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js index d486d02387..325e1ed4a5 100644 --- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js @@ -26,6 +26,7 @@ const droppable: DroppableDimension = getDroppableDimension({ left: 0, }), }); +const origin: Position = { x: 0, y: 0 }; class ScrollableItem extends Component { /* eslint-disable react/sort-comp */ @@ -332,7 +333,7 @@ describe('DraggableDimensionPublisher', () => { render() { return (
- { this.props.children } + {this.props.children}
); } @@ -397,56 +398,52 @@ describe('DraggableDimensionPublisher', () => { } } - it('should clip a dimension by the size of its scroll parent', () => { - const scrollParentRect: ClientRect = getClientRect({ - top: 0, - bottom: 100, - left: 0, - right: 100, - }); - const expected = getDroppableDimension({ - id: droppableId, - clientRect: getClientRect({ - top: 0, - bottom: 100, - left: 0, - // using max value... - right: 110, - }), - }); - (() => { - let count = 0; - jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => { - // first call is item - if (count === 0) { - count++; - // 10px bigger in every direction from the scroll parent - return getClientRect({ - top: -10, - bottom: 110, - left: -10, - right: 110, - }); - } - - // second call is to the scroll parent - return scrollParentRect; - }); - })(); + const noMargin = { + marginTop: '0', + marginRight: '0', + marginBottom: '0', + marginLeft: '0', + }; - jest.spyOn(window, 'getComputedStyle').mockImplementation((el) => { - const noMargin = { - marginTop: '0', - marginRight: '0', - marginBottom: '0', - marginLeft: '0', - }; + type ExecuteArgs = {| + droppableRect: ClientRect, + scrollParentRect: ClientRect, + scrollParentScroll?: Position, + |} + + const execute = ({ + droppableRect, + scrollParentRect, + scrollParentScroll = origin, + }: ExecuteArgs) => { + wrapper = mount( + + ); + + const scrollParentNode: HTMLElement = wrapper.getDOMNode(); + + if (!scrollParentNode.classList.contains('scroll-parent')) { + throw new Error('scroll parent node not obtained correctly'); + } + + const droppableNode: HTMLElement = scrollParentNode.querySelector('.item'); - if (el.className === 'item') { + scrollParentNode.scrollLeft = scrollParentScroll.x; + scrollParentNode.scrollTop = scrollParentScroll.y; + + jest.spyOn(scrollParentNode, 'getBoundingClientRect').mockImplementationOnce(() => scrollParentRect); + jest.spyOn(droppableNode, 'getBoundingClientRect').mockImplementationOnce(() => droppableRect); + + jest.spyOn(window, 'getComputedStyle').mockImplementation((el) => { + if (el === droppableNode) { return noMargin; } - if (el.className === 'scroll-parent') { + if (el === scrollParentNode) { return { ...noMargin, overflow: 'auto', @@ -456,16 +453,219 @@ describe('DraggableDimensionPublisher', () => { throw new Error('unknown el'); }); - wrapper = mount( - - ); wrapper.setProps({ shouldPublish: true, }); + }; + + it('should clip the dimension by the size of the scroll parent', () => { + const droppableRect: ClientRect = getClientRect({ + top: 0, + bottom: 100, + left: 0, + right: 100, + }); + // smaller by 10px in every direction + const scrollParentRect: ClientRect = getClientRect({ + top: 10, + bottom: 90, + left: 10, + right: 90, + }); + const expected = getDroppableDimension({ + id: droppableId, + // because it is smaller in every direction + // the result will be the scroll parent rect + clientRect: scrollParentRect, + }); + + execute({ droppableRect, scrollParentRect }); + + // the trimmed rect + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + + describe('dimension clipping by edge', () => { + const base = { + top: 0, + bottom: 100, + left: 0, + right: 100, + }; + const droppableRect: ClientRect = getClientRect(base); + + describe('cut off by scroll container', () => { + it('should choose the biggest top value', () => { + const scrollParentRect: ClientRect = getClientRect({ + ...base, + top: 10, + }); + execute({ droppableRect, scrollParentRect }); + const expected = getDroppableDimension({ + id: droppableId, + clientRect: getClientRect({ + ...base, + top: 10, + }), + }); + + // the trimmed rect + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + + it('should choose the biggest left value', () => { + const scrollParentRect: ClientRect = getClientRect({ + ...base, + left: 10, + }); + execute({ droppableRect, scrollParentRect }); + const expected = getDroppableDimension({ + id: droppableId, + clientRect: getClientRect({ + ...base, + left: 10, + }), + }); + + // the trimmed rect + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + + it('should choose the smallest right value', () => { + const scrollParentRect: ClientRect = getClientRect({ + ...base, + right: 90, + }); + execute({ droppableRect, scrollParentRect }); + const expected = getDroppableDimension({ + id: droppableId, + clientRect: getClientRect({ + ...base, + right: 90, + }), + }); + + // the trimmed rect + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + + it('should choose the smallest bottom value', () => { + const scrollParentRect: ClientRect = getClientRect({ + ...base, + bottom: 90, + }); + execute({ droppableRect, scrollParentRect }); + const expected = getDroppableDimension({ + id: droppableId, + clientRect: getClientRect({ + ...base, + bottom: 90, + }), + }); + + // the trimmed rect + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + }); + + describe('cut off by droppable rect', () => { + it('should choose the biggest top value', () => { + const scrollParentRect: ClientRect = getClientRect({ + ...base, + top: -10, + }); + execute({ droppableRect, scrollParentRect }); + const expected = getDroppableDimension({ + id: droppableId, + clientRect: getClientRect(base), + }); + + // the trimmed rect + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + + it('should choose the biggest left value', () => { + const scrollParentRect: ClientRect = getClientRect({ + ...base, + left: -10, + }); + execute({ droppableRect, scrollParentRect }); + const expected = getDroppableDimension({ + id: droppableId, + clientRect: getClientRect(base), + }); + + // the trimmed rect + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + + it('should choose the smallest right value', () => { + const scrollParentRect: ClientRect = getClientRect({ + ...base, + right: 110, + }); + execute({ droppableRect, scrollParentRect }); + const expected = getDroppableDimension({ + id: droppableId, + clientRect: getClientRect(base), + }); + + // the trimmed rect + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + + it('should choose the smallest bottom value', () => { + const scrollParentRect: ClientRect = getClientRect({ + ...base, + bottom: 110, + }); + execute({ droppableRect, scrollParentRect }); + const expected = getDroppableDimension({ + id: droppableId, + clientRect: getClientRect(base), + }); + + // the trimmed rect + expect(publish).toBeCalledWith(expected); + expect(publish).toHaveBeenCalledTimes(1); + }); + }); + }); + + it('should take into account the parents scroll when clipping', () => { + const base = { + top: 0, + bottom: 100, + left: 0, + right: 100, + }; + const scrollParentScroll: Position = { + x: 100, + y: 100, + }; + // rect will have the scroll subtracted when measurements are taken + const droppableRect: ClientRect = getClientRect({ + top: base.top - scrollParentScroll.y, + bottom: base.bottom - scrollParentScroll.y, + left: base.left - scrollParentScroll.x, + right: base.right - scrollParentScroll.x, + }); + const scrollParentRect: ClientRect = getClientRect(base); + const expected: DroppableDimension = getDroppableDimension({ + id: droppableId, + scroll: scrollParentScroll, + clientRect: getClientRect(base), + }); + + execute({ droppableRect, scrollParentRect, scrollParentScroll }); expect(publish).toBeCalledWith(expected); expect(publish).toHaveBeenCalledTimes(1); From 6d535d644423fe18af96d6bc73f3f4532dc1c041 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 13 Sep 2017 11:13:20 +1000 Subject: [PATCH 105/117] rearranging to follow AAA --- ...cted-droppable-dimension-publisher.spec.js | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js index 325e1ed4a5..1c7673e280 100644 --- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js @@ -501,7 +501,6 @@ describe('DraggableDimensionPublisher', () => { ...base, top: 10, }); - execute({ droppableRect, scrollParentRect }); const expected = getDroppableDimension({ id: droppableId, clientRect: getClientRect({ @@ -510,6 +509,8 @@ describe('DraggableDimensionPublisher', () => { }), }); + execute({ droppableRect, scrollParentRect }); + // the trimmed rect expect(publish).toBeCalledWith(expected); expect(publish).toHaveBeenCalledTimes(1); @@ -520,7 +521,6 @@ describe('DraggableDimensionPublisher', () => { ...base, left: 10, }); - execute({ droppableRect, scrollParentRect }); const expected = getDroppableDimension({ id: droppableId, clientRect: getClientRect({ @@ -529,6 +529,8 @@ describe('DraggableDimensionPublisher', () => { }), }); + execute({ droppableRect, scrollParentRect }); + // the trimmed rect expect(publish).toBeCalledWith(expected); expect(publish).toHaveBeenCalledTimes(1); @@ -539,7 +541,6 @@ describe('DraggableDimensionPublisher', () => { ...base, right: 90, }); - execute({ droppableRect, scrollParentRect }); const expected = getDroppableDimension({ id: droppableId, clientRect: getClientRect({ @@ -548,6 +549,8 @@ describe('DraggableDimensionPublisher', () => { }), }); + execute({ droppableRect, scrollParentRect }); + // the trimmed rect expect(publish).toBeCalledWith(expected); expect(publish).toHaveBeenCalledTimes(1); @@ -558,7 +561,6 @@ describe('DraggableDimensionPublisher', () => { ...base, bottom: 90, }); - execute({ droppableRect, scrollParentRect }); const expected = getDroppableDimension({ id: droppableId, clientRect: getClientRect({ @@ -567,6 +569,8 @@ describe('DraggableDimensionPublisher', () => { }), }); + execute({ droppableRect, scrollParentRect }); + // the trimmed rect expect(publish).toBeCalledWith(expected); expect(publish).toHaveBeenCalledTimes(1); @@ -579,12 +583,13 @@ describe('DraggableDimensionPublisher', () => { ...base, top: -10, }); - execute({ droppableRect, scrollParentRect }); const expected = getDroppableDimension({ id: droppableId, clientRect: getClientRect(base), }); + execute({ droppableRect, scrollParentRect }); + // the trimmed rect expect(publish).toBeCalledWith(expected); expect(publish).toHaveBeenCalledTimes(1); @@ -595,12 +600,13 @@ describe('DraggableDimensionPublisher', () => { ...base, left: -10, }); - execute({ droppableRect, scrollParentRect }); const expected = getDroppableDimension({ id: droppableId, clientRect: getClientRect(base), }); + execute({ droppableRect, scrollParentRect }); + // the trimmed rect expect(publish).toBeCalledWith(expected); expect(publish).toHaveBeenCalledTimes(1); @@ -611,12 +617,13 @@ describe('DraggableDimensionPublisher', () => { ...base, right: 110, }); - execute({ droppableRect, scrollParentRect }); const expected = getDroppableDimension({ id: droppableId, clientRect: getClientRect(base), }); + execute({ droppableRect, scrollParentRect }); + // the trimmed rect expect(publish).toBeCalledWith(expected); expect(publish).toHaveBeenCalledTimes(1); @@ -627,12 +634,13 @@ describe('DraggableDimensionPublisher', () => { ...base, bottom: 110, }); - execute({ droppableRect, scrollParentRect }); const expected = getDroppableDimension({ id: droppableId, clientRect: getClientRect(base), }); + execute({ droppableRect, scrollParentRect }); + // the trimmed rect expect(publish).toBeCalledWith(expected); expect(publish).toHaveBeenCalledTimes(1); From dcd114fb52b6436e8fba19bbec133bd010366ae1 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 13 Sep 2017 17:30:02 +1000 Subject: [PATCH 106/117] major refactor of get-client-offset --- src/state/action-creators.js | 58 +- src/state/dimension-map-to-list.js | 18 - src/state/get-draggables-inside-droppable.js | 24 +- src/state/get-droppable-over.js | 11 +- src/state/get-new-home-client-center.js | 111 +++ src/state/get-new-home-client-offset.js | 125 --- .../get-best-cross-axis-droppable.js | 5 +- .../move-to-new-droppable/index.js | 1 - stories/src/multiple-horizontal/quote-app.jsx | 47 +- test/unit/state/get-drag-impact.spec.js | 3 +- .../state/get-new-home-client-center.spec.js | 301 ++++++ .../state/get-new-home-client-offset.spec.js | 922 ------------------ .../move-to-best-droppable.spec.js | 2 - 13 files changed, 492 insertions(+), 1136 deletions(-) delete mode 100644 src/state/dimension-map-to-list.js create mode 100644 src/state/get-new-home-client-center.js delete mode 100644 src/state/get-new-home-client-offset.js create mode 100644 test/unit/state/get-new-home-client-center.spec.js delete mode 100644 test/unit/state/get-new-home-client-offset.spec.js delete mode 100644 test/unit/state/move-cross-axis/move-to-best-droppable.spec.js diff --git a/src/state/action-creators.js b/src/state/action-creators.js index a7f6bc4f03..f258c4c68d 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -16,34 +16,33 @@ import type { InitialDrag, } from '../types'; import noImpact from './no-impact'; -import getNewHomeClientOffset from './get-new-home-client-offset'; +import getNewHomeClientCenter from './get-new-home-client-center'; import { add, subtract, isEqual } from './position'; const origin: Position = { x: 0, y: 0 }; -type ScrollDiffResult = {| - droppable: Position, - window: Position, -|} - -const getScrollDiff = ( +type ScrollDiffArgs = {| initial: InitialDrag, current: CurrentDrag, - droppable: DroppableDimension -): ScrollDiffResult => { + droppable: ?DroppableDimension +|} + +const getScrollDiff = ({ + initial, + current, + droppable, +}: ScrollDiffArgs): Position => { const windowScrollDiff: Position = subtract( initial.windowScroll, current.windowScroll ); - const droppableScrollDiff: Position = subtract( + + const droppableScrollDiff: Position = droppable ? subtract( droppable.scroll.initial, droppable.scroll.current - ); + ) : origin; - return { - window: windowScrollDiff, - droppable: droppableScrollDiff, - }; + return add(windowScrollDiff, droppableScrollDiff); }; export type RequestDimensionsAction = {| @@ -309,11 +308,10 @@ export const drop = () => } const { impact, initial, current } = state.drag; - const sourceDroppable: DroppableDimension = - state.dimension.droppable[initial.source.droppableId]; - const destinationDroppable: ?DroppableDimension = impact.destination ? + const droppable: ?DroppableDimension = impact.destination ? state.dimension.droppable[impact.destination.droppableId] : null; + const draggable: DraggableDimension = state.dimension.draggable[current.id]; const result: DropResult = { draggableId: current.id, @@ -322,23 +320,17 @@ export const drop = () => destination: impact.destination, }; - const scrollDiff = getScrollDiff( - initial, - current, - destinationDroppable || sourceDroppable, - ); - - const newHomeOffset: Position = getNewHomeClientOffset({ + const newCenter: Position = getNewHomeClientCenter({ movement: impact.movement, - clientOffset: current.client.offset, - pageOffset: current.page.offset, - droppableScrollDiff: scrollDiff.droppable, - windowScrollDiff: scrollDiff.window, + draggable, draggables: state.dimension.draggable, - destinationDroppable, - draggableId: current.id, + destination: droppable, }); + const clientOffset: Position = subtract(newCenter, draggable.client.withMargin.center); + const scrollDiff: Position = getScrollDiff({ initial, current, droppable }); + const newHomeOffset: Position = add(clientOffset, scrollDiff); + // Do not animate if you do not need to. // This will be the case if either you are dragging with a // keyboard or if you manage to nail it just with a mouse. @@ -394,11 +386,11 @@ export const cancel = () => return; } - const scrollDiff = getScrollDiff(initial, current, droppable); + const scrollDiff: Position = getScrollDiff({ initial, current, droppable }); dispatch(animateDrop({ trigger: 'CANCEL', - newHomeOffset: add(scrollDiff.droppable, scrollDiff.window), + newHomeOffset: scrollDiff, impact: noImpact, result, })); diff --git a/src/state/dimension-map-to-list.js b/src/state/dimension-map-to-list.js deleted file mode 100644 index a556db67f7..0000000000 --- a/src/state/dimension-map-to-list.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow -import memoizeOne from 'memoize-one'; -import type { - DraggableId, - DroppableId, - DraggableDimensionMap, - DroppableDimensionMap, -} from '../types'; - -export const droppableMapToList = memoizeOne( - (droppables: DroppableDimensionMap) => - Object.keys(droppables).map((id: DroppableId) => droppables[id]) -); - -export const draggableMapToList = memoizeOne( - (draggables: DraggableDimensionMap) => - Object.keys(draggables).map((id: DraggableId) => draggables[id]) -); diff --git a/src/state/get-draggables-inside-droppable.js b/src/state/get-draggables-inside-droppable.js index 857ce8f3b6..8bced4371b 100644 --- a/src/state/get-draggables-inside-droppable.js +++ b/src/state/get-draggables-inside-droppable.js @@ -1,7 +1,7 @@ // @flow import memoizeOne from 'memoize-one'; -import { draggableMapToList } from './dimension-map-to-list'; import type { + DraggableId, DraggableDimension, DroppableDimension, DraggableDimensionMap, @@ -10,14 +10,16 @@ import type { export default memoizeOne( (droppable: DroppableDimension, draggables: DraggableDimensionMap, - ): DraggableDimension[] => draggableMapToList(draggables) - .filter((draggable: DraggableDimension): boolean => ( - droppable.id === draggable.droppableId - )) - // Dimensions are not guarenteed to be ordered in the same order as keys - // So we need to sort them so they are in the correct order - .sort((a: DraggableDimension, b: DraggableDimension): number => ( - a.page.withoutMargin.center[droppable.axis.line] - - b.page.withoutMargin.center[droppable.axis.line] - )) + ): DraggableDimension[] => + Object.keys(draggables) + .map((id: DraggableId): DraggableDimension => draggables[id]) + .filter((draggable: DraggableDimension): boolean => ( + droppable.id === draggable.droppableId + )) + // Dimensions are not guarenteed to be ordered in the same order as keys + // So we need to sort them so they are in the correct order + .sort((a: DraggableDimension, b: DraggableDimension): number => ( + a.page.withoutMargin.center[droppable.axis.line] - + b.page.withoutMargin.center[droppable.axis.line] + )) ); diff --git a/src/state/get-droppable-over.js b/src/state/get-droppable-over.js index 73fcde7231..05e127aa47 100644 --- a/src/state/get-droppable-over.js +++ b/src/state/get-droppable-over.js @@ -1,5 +1,4 @@ // @flow -import { droppableMapToList } from './dimension-map-to-list'; import { isPointWithin } from './is-within-visible-bounds-of-droppable'; import type { DroppableId, @@ -12,10 +11,12 @@ export default ( target: Position, droppables: DroppableDimensionMap, ): ?DroppableId => { - const maybe: ?DroppableDimension = droppableMapToList(droppables) - .find((droppable: DroppableDimension): boolean => ( - isPointWithin(droppable)(target) - )); + const maybe: ?DroppableDimension = + Object.keys(droppables) + .map((id: DroppableId): DroppableDimension => droppables[id]) + .find((droppable: DroppableDimension): boolean => ( + isPointWithin(droppable)(target) + )); return maybe ? maybe.id : null; }; diff --git a/src/state/get-new-home-client-center.js b/src/state/get-new-home-client-center.js new file mode 100644 index 0000000000..d4344aa2fe --- /dev/null +++ b/src/state/get-new-home-client-center.js @@ -0,0 +1,111 @@ +// @flow +import type { + Axis, + DimensionFragment, + DraggableDimension, + DraggableDimensionMap, + DragMovement, + DroppableDimension, + Position, +} from '../types'; +import moveToEdge from './move-to-edge'; +import getDraggablesInsideDroppable from './get-draggables-inside-droppable'; + +type NewHomeArgs = {| + movement: DragMovement, + draggable: DraggableDimension, + // all draggables in the system + draggables: DraggableDimensionMap, + destination: ?DroppableDimension, +|}; + +// Returns the client offset required to move an item from its +// original client position to its final resting position +export default ({ + movement, + draggable, + draggables, + destination, +}: NewHomeArgs): Position => { + const homeCenter: Position = draggable.client.withMargin.center; + + // not dropping anywhere + if (!destination) { + return homeCenter; + } + + const { draggables: movedDraggables, isBeyondStartPosition } = movement; + const axis: Axis = destination.axis; + + const isWithinHomeDroppable: boolean = destination.id === draggable.droppableId; + + // dropping back into home index + if (isWithinHomeDroppable && !movedDraggables.length) { + return homeCenter; + } + + // All the draggables in the destination (even the ones that haven't moved) + const draggablesInDestination: DraggableDimension[] = getDraggablesInsideDroppable( + destination, draggables + ); + + // Find the dimension we need to compare the dragged item with + const destinationFragment: DimensionFragment = (() => { + if (isWithinHomeDroppable) { + return draggables[movedDraggables[0]].client.withMargin; + } + + // Not in home list + + if (movedDraggables.length) { + return draggables[movedDraggables[0]].client.withMargin; + } + + // If we're dragging to the last place in a new droppable + // which has items in it (but which haven't moved) + if (draggablesInDestination.length) { + return draggablesInDestination[ + draggablesInDestination.length - 1 + ].client.withMargin; + } + + // Otherwise, return the dimension of the empty foreign droppable + return destination.client.withMargin; + })(); + + const { sourceEdge, destinationEdge } = (() => { + if (isWithinHomeDroppable) { + if (isBeyondStartPosition) { + // move below the target + return { sourceEdge: 'end', destinationEdge: 'end' }; + } + + // move above the target + return { sourceEdge: 'start', destinationEdge: 'start' }; + } + + // not within our home droppable + + // If we're moving in after the last draggable + // we want to move the draggable below the last item + if (!movedDraggables.length && draggablesInDestination.length) { + return { sourceEdge: 'start', destinationEdge: 'end' }; + } + + // move above the target + return { sourceEdge: 'start', destinationEdge: 'start' }; + })(); + + const source: DimensionFragment = draggable.client.withMargin; + + // This is the draggable's new home + const targetCenter: Position = moveToEdge({ + source, + sourceEdge, + destination: destinationFragment, + destinationEdge, + destinationAxis: axis, + }); + + return targetCenter; +}; diff --git a/src/state/get-new-home-client-offset.js b/src/state/get-new-home-client-offset.js deleted file mode 100644 index 260c6161b9..0000000000 --- a/src/state/get-new-home-client-offset.js +++ /dev/null @@ -1,125 +0,0 @@ -// @flow -import type { - DimensionFragment, - DraggableDimension, - DraggableDimensionMap, - DraggableId, - DragMovement, - DroppableDimension, - Position, -} from '../types'; -import { add, subtract } from './position'; -import moveToEdge from './move-to-edge'; -import { draggableMapToList } from './dimension-map-to-list'; - -type NewHomeArgs = {| - movement: DragMovement, - clientOffset: Position, - pageOffset: Position, - draggableId: DraggableId, - droppableScrollDiff: Position, - windowScrollDiff: Position, - draggables: DraggableDimensionMap, - destinationDroppable: ?DroppableDimension, -|}; - -// Returns the client offset required to move an item from its -// original client position to its final resting position -export default ({ - movement, - clientOffset, - pageOffset, - draggableId, - droppableScrollDiff, - windowScrollDiff, - destinationDroppable, - draggables, -}: NewHomeArgs): Position => { - const { draggables: movedDraggables, isBeyondStartPosition } = movement; - const draggedItem: DraggableDimension = draggables[draggableId]; - const isWithinHomeDroppable: boolean = Boolean( - destinationDroppable && - destinationDroppable.id === draggedItem.droppableId - ); - - // If there's no destination or if no movement has occurred, return the starting position. - if (!destinationDroppable || - (isWithinHomeDroppable && !movedDraggables.length)) { - return add(droppableScrollDiff, windowScrollDiff); - } - - const { - axis, - id: destinationDroppableId, - client: destinationDroppableClient, - } = destinationDroppable; - - // All the draggables in the destination (even the ones that haven't moved) - const draggablesInDestination: DraggableDimension[] = draggableMapToList(draggables) - .filter(draggable => draggable.droppableId === destinationDroppableId); - - // The dimension of the item being dragged - const draggedDimensionFragment: DimensionFragment = draggedItem.client.withMargin; - - // Find the dimension we need to compare the dragged item with - const destinationDimensionFragment: DimensionFragment = (() => { - // If we're not dragging into an empty list - if (movedDraggables.length) { - // Return the dimension of the closest item being displaced - // The moved list is ordered by the closest to the furthest - // so we can just grab the first moved item. - return draggables[movedDraggables[0]].client.withMargin; - } - - // If we're dragging to the last place in a new droppable - // which has items in it (but which haven't moved) - if (draggablesInDestination.length) { - return draggablesInDestination[ - draggablesInDestination.length - 1 - ].client.withMargin; - } - - // Otherwise, return the dimension of the empty droppable - return destinationDroppableClient.withMargin; - })(); - - const { sourceEdge, destinationEdge } = (() => { - // If we're moving in after the last draggable in a new droppable - // we match our start edge to its end edge - if (!isWithinHomeDroppable && - !movedDraggables.length && - draggablesInDestination.length) { - return { sourceEdge: 'start', destinationEdge: 'end' }; - } - - // If we're moving forwards in our own list we match end edges - if (isBeyondStartPosition) { - return { sourceEdge: 'end', destinationEdge: 'end' }; - } - - // If we're moving backwards in our own list or into a new list - // we match start edges - return { sourceEdge: 'start', destinationEdge: 'start' }; - })(); - - // This is the draggable's new home - const destination: Position = moveToEdge({ - source: draggedDimensionFragment, - sourceEdge, - destination: destinationDimensionFragment, - destinationEdge, - destinationAxis: axis, - }); - - // The difference between its old position and new position - const distance: Position = subtract(destination, draggedDimensionFragment.center); - - // Accounting for page, client and scroll container offsets - const netPageClientOffset: Position = subtract(clientOffset, pageOffset); - const offsets: Position = add(droppableScrollDiff, netPageClientOffset); - - // Finally, this is how far the dragged item has to travel to be in its new home - const withOffsets: Position = add(distance, offsets); - - return withOffsets; -}; 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 ce9c028a3d..d404f18767 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 @@ -1,10 +1,10 @@ // @flow import { closest } from '../position'; -import { droppableMapToList } from '../dimension-map-to-list'; import isWithin from '../is-within'; import type { Axis, Position, + DroppableId, DimensionFragment, DroppableDimension, DroppableDimensionMap, @@ -39,7 +39,8 @@ export default ({ }: GetBestDroppableArgs): ?DroppableDimension => { const axis: Axis = source.axis; - const candidates: DroppableDimension[] = droppableMapToList(droppables) + const candidates: DroppableDimension[] = Object.keys(droppables) + .map((id: DroppableId) => droppables[id]) // Remove the source droppable from the list .filter((droppable: DroppableDimension): boolean => droppable !== source) // Remove any options that are not enabled diff --git a/src/state/move-cross-axis/move-to-new-droppable/index.js b/src/state/move-cross-axis/move-to-new-droppable/index.js index a06f417493..b073788c77 100644 --- a/src/state/move-cross-axis/move-to-new-droppable/index.js +++ b/src/state/move-cross-axis/move-to-new-droppable/index.js @@ -5,7 +5,6 @@ import { patch } from '../../position'; import type { Result } from '../move-cross-axis-types'; import type { Position, - DragImpact, DraggableDimension, DroppableDimension, DraggableLocation, diff --git a/stories/src/multiple-horizontal/quote-app.jsx b/stories/src/multiple-horizontal/quote-app.jsx index 471534ade7..3e57df280b 100644 --- a/stories/src/multiple-horizontal/quote-app.jsx +++ b/stories/src/multiple-horizontal/quote-app.jsx @@ -5,6 +5,7 @@ import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import QuoteList from './quote-list'; import { colors, grid } from '../constants'; +import reorder from '../reorder'; import type { Quote } from '../types'; import type { DropResult, DragStart, DraggableLocation } from '../../../src/types'; @@ -54,25 +55,41 @@ const resolveDrop = (quotes: GroupedQuotes, const movedQuote = quotes[source.droppableId][source.index]; - Object.entries(newQuotes).forEach(([listId, listQuotes]: [string, Quote[]]) => { - let newListQuotes = [...listQuotes]; + Object.keys(newQuotes).forEach((listId: string) => { + const list: Quote[] = (() => { + const previous: Quote[] = newQuotes[listId]; - if (listId === source.droppableId) { - newListQuotes = [ - ...newListQuotes.slice(0, source.index), - ...newListQuotes.slice(source.index + 1), - ]; - } + // moving within the same list + if (listId === source.droppableId) { + return reorder(previous, source.index, destination.index); + } - if (listId === destination.droppableId) { - newListQuotes = [ - ...newListQuotes.slice(0, destination.index), + // moving to new list + return [ + ...previous.slice(0, destination.index), movedQuote, - ...newListQuotes.slice(destination.index), + ...previous.slice(destination.index), ]; - } - - newQuotes[listId] = newListQuotes; + })(); + + // let list: Quote[] = [...newQuotes[listId]]; + + // if (listId === source.droppableId) { + // list = [ + // ...list.slice(0, source.index), + // ...list.slice(source.index + 1), + // ]; + // } + + // if (listId === destination.droppableId) { + // list = [ + // ...list.slice(0, destination.index), + // movedQuote, + // ...list.slice(destination.index), + // ]; + // } + + newQuotes[listId] = list; }); return newQuotes; diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js index 585c442cb5..448543cd06 100644 --- a/test/unit/state/get-drag-impact.spec.js +++ b/test/unit/state/get-drag-impact.spec.js @@ -5,11 +5,10 @@ import { } from '../../../src/state/dimension'; // eslint-disable-next-line no-duplicate-imports import getDragImpact from '../../../src/state/get-drag-impact'; -import noImpact from '../../../src/state/no-impact'; +import noImpact, { noMovement } from '../../../src/state/no-impact'; import getClientRect from '../../../src/state/get-client-rect'; import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; import { add, patch } from '../../../src/state/position'; -import { noMovement } from '../../../src/state/no-impact'; import type { WithinDroppable, DroppableId, diff --git a/test/unit/state/get-new-home-client-center.spec.js b/test/unit/state/get-new-home-client-center.spec.js new file mode 100644 index 0000000000..38873882b6 --- /dev/null +++ b/test/unit/state/get-new-home-client-center.spec.js @@ -0,0 +1,301 @@ +// @flow +import getNewHomeClientCenter from '../../../src/state/get-new-home-client-center'; +import { noMovement } from '../../../src/state/no-impact'; +import { patch } from '../../../src/state/position'; +import { getDroppableDimension, getDraggableDimension } from '../../../src/state/dimension'; +import { vertical, horizontal } from '../../../src/state/axis'; +import getClientRect from '../../../src/state/get-client-rect'; +import moveToEdge from '../../../src/state/move-to-edge'; +import type { + Axis, + DragMovement, + Position, + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, +} from '../../../src/types'; + +describe('get new home client center', () => { + [vertical, horizontal].forEach((axis: Axis) => { + describe(`dropping on ${axis.direction} list`, () => { + const crossAxisStart: number = 0; + const crossAxisEnd: number = 100; + + const home: DroppableDimension = getDroppableDimension({ + id: 'home', + direction: axis.direction, + clientRect: getClientRect({ + [axis.start]: 0, + [axis.end]: 100, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + }), + }); + + // size 10 + const inHome1: DraggableDimension = getDraggableDimension({ + id: 'inHome1', + droppableId: home.id, + clientRect: getClientRect({ + [axis.start]: 0, + [axis.end]: 10, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + }), + }); + + // size 20 + const inHome2: DraggableDimension = getDraggableDimension({ + id: 'inHome2', + droppableId: home.id, + clientRect: getClientRect({ + [axis.start]: 10, + [axis.end]: 30, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + }), + }); + + // size 30 + const inHome3: DraggableDimension = getDraggableDimension({ + id: 'inHome3', + droppableId: home.id, + clientRect: getClientRect({ + [axis.start]: 30, + [axis.end]: 60, + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + }), + }); + + const draggables: DraggableDimensionMap = { + [inHome1.id]: inHome1, + [inHome2.id]: inHome2, + [inHome3.id]: inHome3, + }; + + const inHome1Size: Position = patch(axis.line, inHome1.page.withMargin[axis.size]); + + it('should return the original center dropped on no destination', () => { + const result: Position = getNewHomeClientCenter({ + movement: noMovement, + draggables, + draggable: inHome1, + destination: null, + }); + + expect(result).toEqual(inHome1.client.withMargin.center); + }); + + describe('dropping in home list', () => { + it('should return the original center if moving back into the same spot', () => { + const newCenter: Position = getNewHomeClientCenter({ + movement: noMovement, + draggables, + draggable: inHome1, + destination: home, + }); + + expect(newCenter).toEqual(inHome1.client.withMargin.center); + }); + + describe('is moving forward (is always beyond start position)', () => { + // moving the first item forward past the third item + it('should move after the closest impacted draggable', () => { + const targetCenter: Position = moveToEdge({ + source: inHome1.client.withMargin, + sourceEdge: 'end', + destination: inHome3.client.withMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + // the movement from the last drag + const movement: DragMovement = { + // ordered by closest to impacted + draggables: [inHome3.id, inHome2.id], + amount: inHome1Size, + isBeyondStartPosition: true, + }; + + const newCenter = getNewHomeClientCenter({ + movement, + draggables, + draggable: inHome1, + destination: home, + }); + + expect(newCenter).toEqual(targetCenter); + }); + }); + + describe('is moving backward (is always not beyond start position)', () => { + // moving inHome3 back past inHome1 + it('should move before the closest impacted draggable', () => { + const targetCenter: Position = moveToEdge({ + source: inHome3.client.withMargin, + sourceEdge: 'start', + destination: inHome1.client.withMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); + // the movement from the last drag + const movement: DragMovement = { + // ordered by closest to impacted + draggables: [inHome1.id, inHome2.id], + amount: inHome1Size, + // is not beyond start position - going backwards + isBeyondStartPosition: false, + }; + + const newCenter = getNewHomeClientCenter({ + movement, + draggables, + draggable: inHome3, + destination: home, + }); + + expect(newCenter).toEqual(targetCenter); + }); + }); + }); + + describe('dropping in foreign list', () => { + const foreignCrossAxisStart: number = 100; + const foreignCrossAxisEnd: number = 200; + const foreign: DroppableDimension = getDroppableDimension({ + id: 'foreign', + direction: axis.direction, + clientRect: getClientRect({ + [axis.start]: 0, + [axis.end]: 100, + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + }), + }); + + // size 10 + const inForeign1: DraggableDimension = getDraggableDimension({ + id: 'inForeign1', + droppableId: foreign.id, + clientRect: getClientRect({ + [axis.start]: 0, + [axis.end]: 10, + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + }), + }); + // size 20 + const inForeign2: DraggableDimension = getDraggableDimension({ + id: 'inForeign2', + droppableId: foreign.id, + clientRect: getClientRect({ + [axis.start]: 0, + [axis.end]: 10, + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + }), + }); + + const withForeign: DraggableDimensionMap = { + ...draggables, + [inForeign1.id]: inForeign1, + [inForeign2.id]: inForeign2, + }; + + describe('is moving into a populated list', () => { + it('should move above the target', () => { + const targetCenter: Position = moveToEdge({ + source: inHome1.client.withMargin, + sourceEdge: 'start', + destination: inForeign1.client.withMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); + // the movement from the last drag + const movement: DragMovement = { + // ordered by closest to impacted + draggables: [inForeign1.id, inForeign2.id], + amount: inHome1Size, + // not relevant when moving into new list + isBeyondStartPosition: false, + }; + + const newCenter = getNewHomeClientCenter({ + movement, + draggables: withForeign, + draggable: inHome1, + destination: foreign, + }); + + expect(newCenter).toEqual(targetCenter); + }); + }); + + describe('is moving to end of a list', () => { + it('should draggable below the last item in the list', () => { + const targetCenter: Position = moveToEdge({ + source: inHome1.client.withMargin, + sourceEdge: 'start', + // will target the last in the foreign droppable + destination: inForeign2.client.withMargin, + destinationEdge: 'end', + destinationAxis: axis, + }); + // the movement from the last drag + const movement: DragMovement = { + // nothing has moved (going to end of list) + draggables: [], + amount: inHome1Size, + // not relevant when moving into new list + isBeyondStartPosition: false, + }; + + const newCenter = getNewHomeClientCenter({ + movement, + draggables: withForeign, + draggable: inHome1, + destination: foreign, + }); + + expect(newCenter).toEqual(targetCenter); + }); + }); + + describe('is moving to empty list', () => { + it('should move to the start of the list', () => { + const empty: DroppableDimension = getDroppableDimension({ + id: 'empty', + clientRect: getClientRect({ + top: 1000, bottom: 2000, left: 1000, right: 1000, + }), + }); + + const targetCenter: Position = moveToEdge({ + source: inHome1.client.withMargin, + sourceEdge: 'start', + destination: empty.client.withMargin, + destinationEdge: 'start', + destinationAxis: axis, + }); + // the movement from the last drag + const movement: DragMovement = { + draggables: [], + amount: inHome1Size, + // not relevant when moving into new list + isBeyondStartPosition: false, + }; + + const newCenter = getNewHomeClientCenter({ + movement, + draggables: withForeign, + draggable: inHome1, + destination: empty, + }); + + expect(newCenter).toEqual(targetCenter); + }); + }); + }); + }); + }); +}); diff --git a/test/unit/state/get-new-home-client-offset.spec.js b/test/unit/state/get-new-home-client-offset.spec.js deleted file mode 100644 index 67dff75812..0000000000 --- a/test/unit/state/get-new-home-client-offset.spec.js +++ /dev/null @@ -1,922 +0,0 @@ -// @flow -import getNewHomeClientOffset from '../../../src/state/get-new-home-client-offset'; -import noImpact from '../../../src/state/no-impact'; -import { add, negate, subtract } from '../../../src/state/position'; -import getDroppableWithDraggables from '../../utils/get-droppable-with-draggables'; -import type { - DragMovement, - Position, -} from '../../../src/types'; - -const origin: Position = { x: 0, y: 0 }; - -const getDistanceOverDraggables = dimension => arr => ({ - [dimension === 'height' ? 'y' : 'x']: arr.reduce( - (total, draggable) => total + draggable.page.withMargin[dimension] + 1, - 0 - ), - [dimension === 'height' ? 'x' : 'y']: 0, -}); -const getVerticalDistanceOverDraggables = getDistanceOverDraggables('height'); -const getHorizontalDistanceOverDraggables = getDistanceOverDraggables('width'); - -describe('get new home client offset', () => { - describe('vertical', () => { - const droppable = getDroppableWithDraggables({ - droppableId: 'drop-1', - droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, - draggableRects: [ - { top: 0, left: 0, bottom: 100, right: 100 }, - { top: 101, left: 0, bottom: 300, right: 100 }, - { top: 301, left: 0, bottom: 600, right: 100 }, - ], - }); - - it('should return the total scroll diff if nothing has moved', () => { - const offset: Position = { - x: 100, - y: 200, - }; - const droppableScrollDiff: Position = { - x: 20, - y: 10, - }; - const windowScrollDiff: Position = { - x: 30, - y: 20, - }; - - const result: Position = getNewHomeClientOffset({ - movement: noImpact.movement, - clientOffset: offset, - pageOffset: offset, - droppableScrollDiff, - windowScrollDiff, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: droppable.droppable, - }); - - expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); - }); - - it('should return the total scroll diff if no destination is provided', () => { - const offset: Position = { - x: 100, - y: 200, - }; - const droppableScrollDiff: Position = { - x: 20, - y: 10, - }; - const windowScrollDiff: Position = { - x: 30, - y: 20, - }; - // There should be no movement without an axis - // This is an error situation - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(1), - amount: { - x: 0, - y: droppable.draggableDimensions[0].page.withMargin.height, - }, - isBeyondStartPosition: true, - }; - - const result: Position = getNewHomeClientOffset({ - movement, - clientOffset: offset, - pageOffset: offset, - droppableScrollDiff, - windowScrollDiff, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: null, - }); - - expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); - }); - - describe('moving forward', () => { - // Moving the first item down into the third position - - // Where the user's cursor is - let selection: Position; - - beforeEach(() => { - selection = { - x: droppable.draggableDimensions[0].client.withoutMargin.center.x, - y: droppable.draggableDimensions[2].client.withoutMargin.top + 1, - }; - }); - - // moving the first item down past the third item - it('should account for the current client location of the dragging item', () => { - // The offset needed to get to the selection. - const clientOffset: Position = subtract( - selection, - droppable.draggableDimensions[0].client.withoutMargin.center - ); - - // this test does not exercise page movement - const pageOffset: Position = clientOffset; - - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(1), - amount: origin, - isBeyondStartPosition: true, - }; - - // How much distance the item needs to travel to be in its new home - // from where it started - const verticalChange = getVerticalDistanceOverDraggables( - droppable.draggableDimensions.slice(1) - ); - // How far away it is from where it needs to end up - const diff: Position = subtract(verticalChange, pageOffset); - // this is the final client offset - const expected = add(clientOffset, diff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - // moving the first item down past the third using only container scroll - it('should account for any changes in the droppables scroll container', () => { - const clientOffset: Position = origin; - const pageOffset: Position = origin; - const droppableScrollDiff: Position = subtract( - selection, - droppable.draggableDimensions[0].page.withoutMargin.center - ); - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(1), - amount: origin, - isBeyondStartPosition: true, - }; - // this is where it needs to end up - const verticalChange = getVerticalDistanceOverDraggables( - droppable.draggableDimensions.slice(1) - ); - // this is how far away it is from where it needs to end up - const diff: Position = subtract(verticalChange, pageOffset); - // this is the final client offset - const expected = add(diff, droppableScrollDiff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - // moving the first item down past the third using only window scroll - it('should account for any changes in the window scroll', () => { - const clientOffset: Position = origin; - const pageOffset: Position = { - x: 10, - y: 200, - }; - const droppableScrollDiff = origin; - const windowScrollDiff = pageOffset; - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(1), - amount: origin, - isBeyondStartPosition: true, - }; - // this is where it needs to end up - const verticalChange = getVerticalDistanceOverDraggables( - droppable.draggableDimensions.slice(1) - ); - // this is how far away it is from where it needs to end up - const diff: Position = subtract(verticalChange, pageOffset); - // this is the final client offset - const expected = add(diff, droppableScrollDiff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - }); - - describe('moving backward', () => { - // Moving the third item back into the first position - - let selection: Position; - - beforeEach(() => { - selection = { - x: droppable.draggableDimensions[2].client.withoutMargin.center.x, - y: droppable.draggableDimensions[0].client.withoutMargin.bottom - 1, - }; - }); - - // moving the third item backwards past the first and second item - it('should account for the current client location of the dragging item', () => { - // The offset needed to get to the selection. - const clientOffset: Position = subtract( - selection, - droppable.draggableDimensions[2].client.withoutMargin.center - ); - - // this test does not exercise page movement - const pageOffset: Position = clientOffset; - - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(0, 2), - amount: origin, - isBeyondStartPosition: false, - }; - - // How much distance the item needs to travel to be in its new home - // from where it started - const verticalChange = negate( - getVerticalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) - ); - // How far away it is from where it needs to end up - const diff: Position = subtract(verticalChange, pageOffset); - // this is the final client offset - const expected = add(clientOffset, diff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[2], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - // moving the third item back past the first and second item using only window scroll - it('should account for the current page location of the dragging item', () => { - // have not moved the item on the screen at all - const clientOffset: Position = origin; - // the window has scrolled to get it to the selection point - const pageOffset: Position = subtract( - selection, - droppable.draggableDimensions[2].page.withoutMargin.center - ); - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(0, 2), - amount: origin, - isBeyondStartPosition: false, - }; - // How much distance the item needs to travel to be in its new home - // from where it started - const verticalChange = negate( - getVerticalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) - ); - // How far away it is from where it needs to end up - const diff: Position = subtract(verticalChange, pageOffset); - // this is the final client offset - const expected = add(clientOffset, diff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[2], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - // moving the third item backwards past the first and second item using only container scroll - it('should account for any changes in the droppables scroll container', () => { - const clientOffset: Position = origin; - const pageOffset: Position = origin; - const droppableScrollDiff: Position = subtract( - selection, - droppable.draggableDimensions[2].page.withoutMargin.center - ); - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(0, 2), - amount: origin, - isBeyondStartPosition: false, - }; - // this is where it needs to end up - const verticalChange = negate( - getVerticalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) - ); - // this is how far away it is from where it needs to end up - const diff: Position = subtract(verticalChange, pageOffset); - // this is the final client offset - const expected = add(diff, droppableScrollDiff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[2], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - }); - }); - - describe('horizontal', () => { - const droppable = getDroppableWithDraggables({ - direction: 'horizontal', - droppableId: 'drop-1', - droppableRect: { top: 0, left: 0, bottom: 100, right: 500 }, - draggableRects: [ - { top: 0, left: 0, bottom: 100, right: 100 }, - { top: 0, left: 101, bottom: 100, right: 300 }, - { top: 0, left: 301, bottom: 100, right: 500 }, - ], - }); - - it('should return to the total scroll diff if nothing has moved', () => { - const offset: Position = { - x: 100, - y: 200, - }; - const droppableScrollDiff: Position = { - x: 20, - y: 10, - }; - const windowScrollDiff: Position = { - x: 30, - y: 20, - }; - - const result: Position = getNewHomeClientOffset({ - movement: noImpact.movement, - clientOffset: offset, - pageOffset: offset, - droppableScrollDiff, - windowScrollDiff, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: droppable.droppable, - }); - - expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); - }); - - it('should return the total scroll diff is no destination is provided', () => { - const offset: Position = { - x: 100, - y: 200, - }; - const droppableScrollDiff: Position = { - x: 20, - y: 10, - }; - const windowScrollDiff: Position = { - x: 30, - y: 20, - }; - // There should be no movement without an axis - // This is an error situation - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(1), - amount: { - x: droppable.draggableDimensions[0].page.withMargin.width, - y: 0, - }, - isBeyondStartPosition: true, - }; - - const result: Position = getNewHomeClientOffset({ - movement, - clientOffset: offset, - pageOffset: offset, - droppableScrollDiff, - windowScrollDiff, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: null, - }); - - expect(result).toEqual(add(droppableScrollDiff, windowScrollDiff)); - }); - - describe('moving forward', () => { - // Moving the first item forward into the third position - - // Where the user's cursor is - let selection: Position; - - beforeEach(() => { - selection = { - x: droppable.draggableDimensions[2].client.withoutMargin.left + 1, - y: droppable.draggableDimensions[0].client.withoutMargin.center.y, - }; - }); - - // moving the first item down past the third item - it('should account for the current client location of the dragging item', () => { - // The offset needed to get to the selection. - const clientOffset: Position = subtract( - selection, - droppable.draggableDimensions[0].client.withoutMargin.center - ); - - // this test does not exercise page movement - const pageOffset: Position = clientOffset; - - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(1), - amount: origin, - isBeyondStartPosition: true, - }; - - // How much distance the item needs to travel to be in its new home - // from where it started - const horizontalChange = getHorizontalDistanceOverDraggables( - droppable.draggableDimensions.slice(1) - ); - // How far away it is from where it needs to end up - const diff: Position = subtract(horizontalChange, pageOffset); - // this is the final client offset - const expected = add(clientOffset, diff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - // moving the first item down past the third using only container scroll - it('should account for any changes in the droppables scroll container', () => { - const clientOffset: Position = origin; - const pageOffset: Position = origin; - const droppableScrollDiff: Position = subtract( - selection, - droppable.draggableDimensions[0].page.withoutMargin.center - ); - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(1), - amount: origin, - isBeyondStartPosition: true, - }; - // this is where it needs to end up - const horizontalChange = getHorizontalDistanceOverDraggables( - droppable.draggableDimensions.slice(1) - ); - // this is how far away it is from where it needs to end up - const diff: Position = subtract(horizontalChange, pageOffset); - // this is the final client offset - const expected = add(diff, droppableScrollDiff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - // moving the first item forward past the third using only window scroll - it('should account for any changes in the window scroll', () => { - const clientOffset: Position = origin; - const pageOffset: Position = { - x: 10, - y: 200, - }; - const droppableScrollDiff = origin; - const windowScrollDiff = pageOffset; - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(1), - amount: origin, - isBeyondStartPosition: true, - }; - // this is where it needs to end up - const horizontalChange = getHorizontalDistanceOverDraggables( - droppable.draggableDimensions.slice(1) - ); - // this is how far away it is from where it needs to end up - const diff: Position = subtract(horizontalChange, pageOffset); - // this is the final client offset - const expected = add(diff, droppableScrollDiff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[0], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - }); - - describe('moving backward', () => { - // Moving the third item back into the first position - - // Where the user's cursor is - let selection: Position; - - beforeEach(() => { - selection = { - x: droppable.draggableDimensions[0].client.withoutMargin.right - 1, - y: droppable.draggableDimensions[2].client.withoutMargin.center.y, - }; - }); - - // moving the third item back past the first and second item - it('should account for the current client location of the dragging item', () => { - // The offset needed to get to the selection. - const clientOffset: Position = subtract( - selection, - droppable.draggableDimensions[2].client.withoutMargin.center - ); - - // this test does not exercise page movement - const pageOffset: Position = clientOffset; - - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(0, 2), - amount: origin, - isBeyondStartPosition: false, - }; - - // How much distance the item needs to travel to be in its new home - // from where it started - const horizontalChange = negate( - getHorizontalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) - ); - // How far away it is from where it needs to end up - const diff: Position = subtract(horizontalChange, pageOffset); - // this is the final client offset - const expected = add(clientOffset, diff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[2], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - // moving the third item back past the first and second item using only window scroll - it('should account for the current page location of the dragging item', () => { - // have not moved the item on the screen at all - const clientOffset: Position = origin; - // the window has scrolled to get it to the selection point - const pageOffset: Position = subtract( - selection, - droppable.draggableDimensions[2].page.withoutMargin.center - ); - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(0, 2), - amount: origin, - isBeyondStartPosition: false, - }; - // How much distance the item needs to travel to be in its new home - // from where it started - const horizontalChange = negate( - getHorizontalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) - ); - // How far away it is from where it needs to end up - const diff: Position = subtract(horizontalChange, pageOffset); - // this is the final client offset - const expected = add(clientOffset, diff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[2], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - // moving the first item down past the third using only container scroll - it('should account for any changes in the droppables scroll container', () => { - const clientOffset: Position = origin; - const pageOffset: Position = origin; - const droppableScrollDiff: Position = subtract( - selection, - droppable.draggableDimensions[2].page.withoutMargin.center - ); - const movement: DragMovement = { - draggables: droppable.draggableIds.slice(0, 2), - amount: origin, - isBeyondStartPosition: false, - }; - // this is where it needs to end up - const horizontalChange = negate( - getHorizontalDistanceOverDraggables(droppable.draggableDimensions.slice(0, 2)) - ); - // this is how far away it is from where it needs to end up - const diff: Position = subtract(horizontalChange, pageOffset); - // this is the final client offset - const expected = add(diff, droppableScrollDiff); - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff, - windowScrollDiff: origin, - draggables: droppable.draggables, - draggableId: droppable.draggableIds[2], - destinationDroppable: droppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - }); - }); - - describe('multiple lists - vertical', () => { - const homeDroppable = getDroppableWithDraggables({ - droppableId: 'drop-home', - droppableRect: { top: 0, left: 0, bottom: 600, right: 100 }, - draggableRects: [ - { top: 0, left: 0, bottom: 100, right: 100 }, - { top: 101, left: 0, bottom: 300, right: 100 }, - { top: 301, left: 0, bottom: 600, right: 100 }, - ], - }); - - const destinationDroppable = getDroppableWithDraggables({ - droppableId: 'drop-destination', - droppableRect: { top: 100, left: 110, bottom: 700, right: 210 }, - draggableRects: [ - { top: 100, left: 110, bottom: 400, right: 210 }, - { top: 401, left: 110, bottom: 600, right: 210 }, - { top: 601, left: 110, bottom: 700, right: 210 }, - ], - }); - - const emptyDroppable = getDroppableWithDraggables({ - droppableId: 'drop-empty', - droppableRect: { top: 200, left: 220, bottom: 800, right: 320 }, - draggableRects: [], - }); - - const draggable = homeDroppable.draggableDimensions[0]; - - const allDraggables = { ...homeDroppable.draggables, ...destinationDroppable.draggables }; - - const selection = { - x: emptyDroppable.droppable.client.withMargin.left + 1, - y: emptyDroppable.droppable.client.withMargin.bottom + 1, - }; - - const clientOffset = subtract(selection, draggable.client.withMargin.center); - - const pageOffset = clientOffset; - - it('should move to the top of the droppable when landing in an empty droppable', () => { - const movement: DragMovement = { - draggables: emptyDroppable.draggableIds, - amount: origin, - isBeyondStartPosition: false, - }; - - // Align top-left of incoming draggable with top-left of droppable - const expected = { - x: ( - emptyDroppable.droppable.client.withMargin.left - - draggable.client.withMargin.left - ), - y: ( - emptyDroppable.droppable.client.withMargin.top - - draggable.client.withMargin.top - ), - }; - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff: origin, - draggables: allDraggables, - draggableId: draggable.id, - destinationDroppable: emptyDroppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - it('should move into the top of the first-most displaced draggable when landing in a new droppable which already contains draggables', () => { - const movement: DragMovement = { - draggables: destinationDroppable.draggableIds.slice(2), - amount: origin, - isBeyondStartPosition: false, - }; - - const displacedDraggable = destinationDroppable.draggableDimensions[2]; - - // Align top-left of incoming draggable with top-left of displaced draggable - const expected = { - x: ( - displacedDraggable.client.withMargin.left - - draggable.client.withMargin.left - ), - y: ( - displacedDraggable.client.withMargin.top - - draggable.client.withMargin.top - ), - }; - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff: origin, - draggables: allDraggables, - draggableId: draggable.id, - destinationDroppable: destinationDroppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - it('should move in after the last draggable when landing on the end of a new droppable which already contains draggables', () => { - const movement: DragMovement = { - draggables: [], - amount: origin, - isBeyondStartPosition: false, - }; - - const lastDraggable = destinationDroppable.draggableDimensions[2]; - - // Align top-left of incoming draggable with bottom-left of last draggable - const expected = { - x: ( - lastDraggable.client.withMargin.left - - draggable.client.withMargin.left - ), - y: ( - lastDraggable.client.withMargin.bottom - - draggable.client.withMargin.top - ), - }; - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff: origin, - draggables: allDraggables, - draggableId: draggable.id, - destinationDroppable: destinationDroppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - it('should account for internal scrolling in the destination droppable', () => { - const movement: DragMovement = { - draggables: destinationDroppable.draggableIds.slice(2), - amount: origin, - isBeyondStartPosition: false, - }; - - const displacedDraggable = destinationDroppable.draggableDimensions[2]; - - const horizontalScrollAmount = 50; - const verticalScrollAmount = 100; - - const droppableScrollDiff = { - x: -horizontalScrollAmount, - y: -verticalScrollAmount, - }; - - // Offset alignment by scroll amount - const expected = { - x: ( - displacedDraggable.client.withMargin.left - - draggable.client.withMargin.left - - horizontalScrollAmount - ), - y: ( - displacedDraggable.client.withMargin.top - - draggable.client.withMargin.top - - verticalScrollAmount - ), - }; - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff, - windowScrollDiff: origin, - draggables: allDraggables, - draggableId: draggable.id, - destinationDroppable: destinationDroppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - - it('should account for full page scrolling during the drag', () => { - const movement: DragMovement = { - draggables: destinationDroppable.draggableIds.slice(2), - amount: origin, - isBeyondStartPosition: false, - }; - - const displacedDraggable = destinationDroppable.draggableDimensions[2]; - - const windowScrollDiff = { - x: -100, - y: -200, - }; - - // Since the whole page scrolled there should be no net difference in alignment - const expected = { - x: ( - displacedDraggable.client.withMargin.left - - draggable.client.withMargin.left - ), - y: ( - displacedDraggable.client.withMargin.top - - draggable.client.withMargin.top - ), - }; - - const newHomeOffset = getNewHomeClientOffset({ - movement, - clientOffset, - pageOffset, - droppableScrollDiff: origin, - windowScrollDiff, - draggables: allDraggables, - draggableId: draggable.id, - destinationDroppable: destinationDroppable.droppable, - }); - - expect(newHomeOffset).toEqual(expected); - }); - }); -}); diff --git a/test/unit/state/move-cross-axis/move-to-best-droppable.spec.js b/test/unit/state/move-cross-axis/move-to-best-droppable.spec.js deleted file mode 100644 index ceaff2d3b9..0000000000 --- a/test/unit/state/move-cross-axis/move-to-best-droppable.spec.js +++ /dev/null @@ -1,2 +0,0 @@ -// need to add tests for checks done in move-to-best-droppable -// eg returning null when no matches are found From f21a6297ea80f987f194020d04b085421d5ac921 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Wed, 13 Sep 2017 21:08:50 +1000 Subject: [PATCH 107/117] fixing linting and flow --- src/state/get-new-home-client-center.js | 3 +- stories/src/multiple-horizontal/quote-app.jsx | 26 +-------------- stories/src/multiple-vertical/quote-app.jsx | 33 +++++++++---------- 3 files changed, 19 insertions(+), 43 deletions(-) diff --git a/src/state/get-new-home-client-center.js b/src/state/get-new-home-client-center.js index d4344aa2fe..feafb1e7a3 100644 --- a/src/state/get-new-home-client-center.js +++ b/src/state/get-new-home-client-center.js @@ -30,7 +30,7 @@ export default ({ const homeCenter: Position = draggable.client.withMargin.center; // not dropping anywhere - if (!destination) { + if (destination == null) { return homeCenter; } @@ -70,6 +70,7 @@ export default ({ } // Otherwise, return the dimension of the empty foreign droppable + // $ExpectError - flow does not correctly type this as non optional return destination.client.withMargin; })(); diff --git a/stories/src/multiple-horizontal/quote-app.jsx b/stories/src/multiple-horizontal/quote-app.jsx index 3e57df280b..c0e14156c1 100644 --- a/stories/src/multiple-horizontal/quote-app.jsx +++ b/stories/src/multiple-horizontal/quote-app.jsx @@ -23,16 +23,9 @@ const Root = styled.div` flex-direction: column; `; -const Row = styled.div` - margin: ${grid * 2}px ${grid * 2}px; -`; - -const PushDown = styled.div` - height: 200px; -`; - const isDraggingClassName = 'is-dragging'; +/* eslint-disable react/no-unused-prop-types */ type GroupedQuotes = { alpha: Quote[], beta: Quote[], @@ -72,23 +65,6 @@ const resolveDrop = (quotes: GroupedQuotes, ]; })(); - // let list: Quote[] = [...newQuotes[listId]]; - - // if (listId === source.droppableId) { - // list = [ - // ...list.slice(0, source.index), - // ...list.slice(source.index + 1), - // ]; - // } - - // if (listId === destination.droppableId) { - // list = [ - // ...list.slice(0, destination.index), - // movedQuote, - // ...list.slice(destination.index), - // ]; - // } - newQuotes[listId] = list; }); diff --git a/stories/src/multiple-vertical/quote-app.jsx b/stories/src/multiple-vertical/quote-app.jsx index 1ea6ac60c1..8e5eb4f2c5 100644 --- a/stories/src/multiple-vertical/quote-app.jsx +++ b/stories/src/multiple-vertical/quote-app.jsx @@ -5,6 +5,7 @@ import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import QuoteList from './quote-list'; import { colors, grid } from '../constants'; +import reorder from '../reorder'; import type { Quote } from '../types'; import type { DropResult, DragStart, DraggableLocation } from '../../../src/types'; @@ -33,6 +34,7 @@ const PushDown = styled.div` const isDraggingClassName = 'is-dragging'; +/* eslint-disable react/no-unused-prop-types */ type GroupedQuotes = { alpha: Quote[], beta: Quote[], @@ -57,25 +59,24 @@ const resolveDrop = (quotes: GroupedQuotes, const movedQuote = quotes[source.droppableId][source.index]; - Object.entries(newQuotes).forEach(([listId, listQuotes]: [string, Quote[]]) => { - let newListQuotes = [...listQuotes]; + Object.keys(newQuotes).forEach((listId: string) => { + const list: Quote[] = (() => { + const previous: Quote[] = newQuotes[listId]; - if (listId === source.droppableId) { - newListQuotes = [ - ...newListQuotes.slice(0, source.index), - ...newListQuotes.slice(source.index + 1), - ]; - } + // moving within the same list + if (listId === source.droppableId) { + return reorder(previous, source.index, destination.index); + } - if (listId === destination.droppableId) { - newListQuotes = [ - ...newListQuotes.slice(0, destination.index), + // moving to new list + return [ + ...previous.slice(0, destination.index), movedQuote, - ...newListQuotes.slice(destination.index), + ...previous.slice(destination.index), ]; - } + })(); - newQuotes[listId] = newListQuotes; + newQuotes[listId] = list; }); return newQuotes; @@ -132,12 +133,10 @@ export default class QuoteApp extends Component { if (!sourceDroppable) { return null; } - return null; const droppables: string[] = ['alpha', 'beta', 'gamma', 'delta']; const sourceIndex = droppables.indexOf(sourceDroppable); - // const disabledDroppableIndex = (sourceIndex + 1) % droppables.length; - const disabledDroppableIndex = sourceIndex; + const disabledDroppableIndex = (sourceIndex + 1) % droppables.length; return droppables[disabledDroppableIndex]; } From 3228ec2bd30079567e2c57dcdcf52518e9c3cace Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 09:14:20 +1000 Subject: [PATCH 108/117] reworking stories --- stories/2-single-horizontal-story.js | 12 ++-- stories/4-board-story.js | 15 ++++- stories/src/board/board.jsx | 67 ++++++++++++++----- stories/src/data.js | 10 +-- stories/src/horizontal/author-app.jsx | 48 ++++--------- stories/src/horizontal/author-list.jsx | 43 ------------ stories/src/horizontal/types.js | 2 - stories/src/multiple-horizontal/quote-app.jsx | 11 ++- stories/src/multiple-vertical/quote-app.jsx | 2 +- .../author-list.jsx} | 12 +--- stories/src/{board => primatives}/column.jsx | 21 +++--- .../quote-list.jsx | 58 ++++++---------- stories/src/types.js | 4 ++ stories/src/vertical/quote-app.jsx | 2 +- stories/src/vertical/quote-list.jsx | 14 +--- 15 files changed, 137 insertions(+), 184 deletions(-) delete mode 100644 stories/src/horizontal/author-list.jsx delete mode 100644 stories/src/horizontal/types.js rename stories/src/{multiple-horizontal/quote-list.jsx => primatives/author-list.jsx} (91%) rename stories/src/{board => primatives}/column.jsx (81%) rename stories/src/{multiple-vertical => primatives}/quote-list.jsx (61%) diff --git a/stories/2-single-horizontal-story.js b/stories/2-single-horizontal-story.js index a0f2084603..b9a4e93591 100644 --- a/stories/2-single-horizontal-story.js +++ b/stories/2-single-horizontal-story.js @@ -3,10 +3,10 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import styled from 'styled-components'; import AuthorApp from './src/horizontal/author-app'; -import { authors, getAuthors } from './src/data'; -import type { Author } from './src/types'; +import { quotes, getQuotes } from './src/data'; +import type { Quote } from './src/types'; -const bigData: Author[] = getAuthors(30); +const bigData: Quote[] = getQuotes(30); const WideWindow = styled.div` width: 120vw; @@ -14,13 +14,13 @@ const WideWindow = styled.div` storiesOf('single horizontal list', module) .add('simple example', () => ( - + )) .add('with overflow scroll', () => ( - + )) .add('with window scroll and overflow scroll', () => ( - + )); diff --git a/stories/4-board-story.js b/stories/4-board-story.js index 5515b5065d..87767f519e 100644 --- a/stories/4-board-story.js +++ b/stories/4-board-story.js @@ -2,9 +2,20 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import Board from './src/board/board'; -import { authorWithQuotes } from './src/data'; +import { getQuotes } from './src/data'; +import type { QuoteMap } from './src/types'; + +const todo: string = 'Todo'; +const inProgress: string = 'In progress'; +const done: string = 'Done'; + +const columns: QuoteMap = { + [todo]: getQuotes(0), + [inProgress]: getQuotes(3), + [done]: getQuotes(4), +}; storiesOf('board', module) .add('task board', () => ( - + )); diff --git a/stories/src/board/board.jsx b/stories/src/board/board.jsx index 604457b027..a3cf8c92db 100644 --- a/stories/src/board/board.jsx +++ b/stories/src/board/board.jsx @@ -2,9 +2,9 @@ import React, { Component } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { action } from '@storybook/addon-actions'; -import Column from './column'; +import Column from '../primatives/column'; import { colors } from '../constants'; -import reorder, { reorderGroup } from '../reorder'; +import reorder from '../reorder'; import { DragDropContext, Droppable } from '../../../src/'; import type { DropResult, @@ -12,7 +12,7 @@ import type { DraggableLocation, DroppableProvided, } from '../../../src/'; -import type { AuthorWithQuotes } from '../types'; +import type { Quote, QuoteMap } from '../types'; const isDraggingClassName = 'is-dragging'; @@ -29,11 +29,12 @@ const Container = styled.div` `; type Props = {| - initial: AuthorWithQuotes[], + initial: QuoteMap, |} type State = {| - columns: AuthorWithQuotes[], + columns: QuoteMap, + ordered: string[], |} export default class Board extends Component { @@ -43,6 +44,7 @@ export default class Board extends Component { state: State = { columns: this.props.initial, + ordered: Object.keys(this.props.initial), } /* eslint-enable react/sort-comp */ @@ -76,34 +78,63 @@ export default class Board extends Component { const destination: DraggableLocation = result.destination; // reordering column - if (result.type === 'AUTHOR') { - const columns: AuthorWithQuotes[] = reorder( - this.state.columns, + if (result.type === 'COLUMN') { + const ordered: string[] = reorder( + this.state.ordered, source.index, destination.index ); this.setState({ - columns, + ordered, }); return; } - const columns: ?AuthorWithQuotes[] = reorderGroup( - this.state.columns, result - ); + const current: Quote[] = [...this.state.columns[source.droppableId]]; + console.log('current', current); - if (!columns) { + // reordering within the same column + if (source.droppableId === destination.droppableId) { + const reordered: Quote[] = reorder( + current, + source.index, + destination.index, + ); + const columns: QuoteMap = { + ...this.state.columns, + [source.droppableId]: reordered, + }; + this.setState({ + columns, + }); return; } + const target: Quote = current[source.index]; + const next: Quote[] = [...this.state.columns[destination.droppableId]]; + + // remove from original + current.splice(source.index, 1); + // insert into next + next.splice(destination.index, 0, target); + + const columns: QuoteMap = { + ...this.state.columns, + [source.droppableId]: current, + [destination.droppableId]: next, + }; + this.setState({ columns, }); } render() { + const columns: QuoteMap = this.state.columns; + const ordered: string[] = this.state.ordered; + return ( {(provided: DroppableProvided) => ( - {this.state.columns.map((column: AuthorWithQuotes) => ( - + {ordered.map((key: string) => ( + ))} )} diff --git a/stories/src/data.js b/stories/src/data.js index 366c2c4253..6c7670ff43 100644 --- a/stories/src/data.js +++ b/stories/src/data.js @@ -96,12 +96,14 @@ export const quotes: Quote[] = [ }, ]; +let idCount: number = 0; + export const getQuotes = (count: number): Quote[] => - Array.from({ length: count }, (v, k) => k).map((val: number) => { + Array.from({ length: count }, (v, k) => k).map(() => { const random: Quote = quotes[Math.floor(Math.random() * quotes.length)]; const custom: Quote = { - id: `${val}`, + id: `quote-${idCount++}`, content: random.content, author: random.author, }; @@ -110,11 +112,11 @@ export const getQuotes = (count: number): Quote[] => }); export const getAuthors = (count: number): Author[] => - Array.from({ length: count }, (v, k) => k).map((val: number) => { + Array.from({ length: count }, (v, k) => k).map(() => { const random: Author = authors[Math.floor(Math.random() * authors.length)]; const custom: Author = { - id: `${val}`, + id: `author-${idCount++}`, name: random.name, avatarUrl: random.avatarUrl, url: random.url, diff --git a/stories/src/horizontal/author-app.jsx b/stories/src/horizontal/author-app.jsx index c2ad48b5a6..08022bfc32 100644 --- a/stories/src/horizontal/author-app.jsx +++ b/stories/src/horizontal/author-app.jsx @@ -2,31 +2,27 @@ import React, { Component } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { action } from '@storybook/addon-actions'; -import { Draggable, DragDropContext } from '../../../src/'; +import { DragDropContext } from '../../../src/'; import type { DropResult, DragStart, - DraggableProvided, - DraggableStateSnapshot, } from '../../../src'; -import AuthorList from './author-list'; -import AuthorItem from '../primatives/author-item'; +import type { Quote } from '../types'; +import AuthorList from '../primatives/author-list'; import reorder from '../reorder'; import { colors, grid } from '../constants'; -import type { Author } from '../types'; -import type { Overflow } from './types'; const isDraggingClassName = 'is-dragging'; const publishOnDragStart = action('onDragStart'); const publishOnDragEnd = action('onDragEnd'); type Props = {| - initial: Author[], - overflow?: Overflow, + initial: Quote[], + internalScroll?: boolean, |} type State = {| - authors: Author[], + quotes: Quote[], |} const Root = styled.div` @@ -40,7 +36,7 @@ export default class AuthorApp extends Component { state: State state: State = { - authors: this.props.initial, + quotes: this.props.initial, } /* eslint-enable react/sort-comp */ @@ -70,14 +66,14 @@ export default class AuthorApp extends Component { return; } - const authors = reorder( - this.state.authors, + const quotes = reorder( + this.state.quotes, result.source.index, result.destination.index ); this.setState({ - authors, + quotes, }); } @@ -88,26 +84,10 @@ export default class AuthorApp extends Component { onDragEnd={this.onDragEnd} > - - {this.state.authors.map((author: Author) => ( - - {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( -
- - {provided.placeholder} -
- )} - -
- ))} -
+
); diff --git a/stories/src/horizontal/author-list.jsx b/stories/src/horizontal/author-list.jsx deleted file mode 100644 index f2ba588e2e..0000000000 --- a/stories/src/horizontal/author-list.jsx +++ /dev/null @@ -1,43 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import styled from 'styled-components'; -import { Droppable } from '../../../src/'; -import type { DroppableProvided, DroppableStateSnapshot } from '../../../src/'; -import type { Overflow } from './types'; - -const Container = styled.div` - display: flex; - overflow: ${({ overflow }) => overflow}; -`; - -export default class AuthorList extends Component { - props: {| - listId: string, - overflow?: Overflow, - children?: any, - |} - - static defaultProps = { - overflow: 'visible', - } - - render() { - return ( - - {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( - - {this.props.children} - {provided.placeholder} - - )} - - ); - } -} diff --git a/stories/src/horizontal/types.js b/stories/src/horizontal/types.js deleted file mode 100644 index ca8b9349e0..0000000000 --- a/stories/src/horizontal/types.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export type Overflow = 'auto' | 'scroll' | 'visible'; diff --git a/stories/src/multiple-horizontal/quote-app.jsx b/stories/src/multiple-horizontal/quote-app.jsx index c0e14156c1..769e61044e 100644 --- a/stories/src/multiple-horizontal/quote-app.jsx +++ b/stories/src/multiple-horizontal/quote-app.jsx @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; -import QuoteList from './quote-list'; +import AuthorList from '../primatives/author-list'; import { colors, grid } from '../constants'; import reorder from '../reorder'; import type { Quote } from '../types'; @@ -121,18 +121,15 @@ export default class QuoteApp extends Component { onDragEnd={this.onDragEnd} > - - - { - const { listId, listType, quotes } = this.props; + const { listType, quotes } = this.props; return ( - {listId} {quotes.map((quote: Quote) => ( diff --git a/stories/src/board/column.jsx b/stories/src/primatives/column.jsx similarity index 81% rename from stories/src/board/column.jsx rename to stories/src/primatives/column.jsx index 94474c73e8..ba7c92001c 100644 --- a/stories/src/board/column.jsx +++ b/stories/src/primatives/column.jsx @@ -4,8 +4,8 @@ import styled from 'styled-components'; import { grid, colors, borderRadius } from '../constants'; import { Draggable } from '../../../src/'; import type { DraggableProvided, DraggableStateSnapshot } from '../../../src/'; -import CardList from '../vertical/quote-list'; -import type { AuthorWithQuotes } from '../types'; +import QuoteList from './quote-list'; +import type { Quote } from '../types'; const Wrapper = styled.div` display: flex; @@ -42,12 +42,15 @@ const Title = styled.h4` export default class Column extends Component { props: {| - column: AuthorWithQuotes + title: string, + quotes: Quote[] |} + render() { - const column: AuthorWithQuotes = this.props.column; + const title: string = this.props.title; + const quotes: Quote[] = this.props.quotes; return ( - + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( - {column.author.name} + {title} - {provided.placeholder} diff --git a/stories/src/multiple-vertical/quote-list.jsx b/stories/src/primatives/quote-list.jsx similarity index 61% rename from stories/src/multiple-vertical/quote-list.jsx rename to stories/src/primatives/quote-list.jsx index 3f3942f034..48db3c21d4 100644 --- a/stories/src/multiple-vertical/quote-list.jsx +++ b/stories/src/primatives/quote-list.jsx @@ -16,7 +16,7 @@ const Wrapper = styled.div` background-color: ${({ isDraggingOver }) => (isDraggingOver ? colors.blue.lighter : colors.blue.light)}; display: flex; flex-direction: column; - opacity: ${({ isDropDisabled }) => (isDropDisabled ? 0.5 : 1)}; + opacity: ${({ isDropDisabled }) => (isDropDisabled ? 0.5 : 'inherit')}; padding: ${grid}px; padding-bottom: 0; transition: background-color 0.1s ease, opacity 0.1s ease; @@ -24,7 +24,7 @@ const Wrapper = styled.div` width: 250px; `; -const DropZone = styled.div` +const Container = styled.div` /* stop the list collapsing when empty */ min-height: 250px; `; @@ -34,19 +34,6 @@ const ScrollContainer = styled.div` max-height: 800px; `; -const Container = styled.div` - /* flex child */ - flex-grow: 1; - - /* flex parent */ - display: flex; - flex-direction: column; -`; - -const Title = styled.h4` - margin-bottom: ${grid}px; -`; - export default class QuoteList extends Component { props: {| listId: string, @@ -57,30 +44,27 @@ export default class QuoteList extends Component { style?: Object, |} - renderBoard = (dropProvided: DroppableProvided) => { - const { listId, listType, quotes } = this.props; + renderQuotes = (dropProvided: DroppableProvided) => { + const { listType, quotes } = this.props; return ( - - {listId} - - {quotes.map((quote: Quote) => ( - - {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( -
- - {dragProvided.placeholder} -
+ + {quotes.map((quote: Quote) => ( + + {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( +
+ + {dragProvided.placeholder} +
)} -
+
))} - {dropProvided.placeholder} -
+ {dropProvided.placeholder}
); } @@ -98,10 +82,10 @@ export default class QuoteList extends Component { > {internalScroll ? ( - {this.renderBoard(dropProvided)} + {this.renderQuotes(dropProvided)} ) : ( - this.renderBoard(dropProvided) + this.renderQuotes(dropProvided) )}
)} diff --git a/stories/src/types.js b/stories/src/types.js index bbcb178cd7..bbe3ccf8a8 100644 --- a/stories/src/types.js +++ b/stories/src/types.js @@ -23,3 +23,7 @@ export type AuthorWithQuotes = {| author: Author, quotes: Quote[], |} + +export type QuoteMap = { + [key: string]: Quote[] +} diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index 77d4a41544..3d1bf9d5a5 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; -import QuoteList from './quote-list'; +import QuoteList from '../primatives/quote-list'; import { colors, grid } from '../constants'; import reorder from '../reorder'; import type { Quote } from '../types'; diff --git a/stories/src/vertical/quote-list.jsx b/stories/src/vertical/quote-list.jsx index 5a94487bba..c69c176e51 100644 --- a/stories/src/vertical/quote-list.jsx +++ b/stories/src/vertical/quote-list.jsx @@ -21,16 +21,7 @@ const Container = styled.div` user-select: none; transition: background-color 0.1s ease; width: 250px; -`; - -const AddCard = styled.button` - margin-bottom: ${grid}px; - margin-top: ${grid}px; - outline: none; - border: none; - font-size: 14px; - text-align: left; - padding: ${grid * 1.5}px ${grid}px; + min-height: 250px; `; export default class QuoteList extends Component { @@ -67,9 +58,6 @@ export default class QuoteList extends Component {
))} {dropProvided.placeholder} - - Add a card... -
)} From 20c9d45aa22d5c0afa70c298752fcae1a1e65f22 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 09:21:37 +1000 Subject: [PATCH 109/117] fixing stories --- stories/src/horizontal/author-app.jsx | 1 + stories/src/multiple-horizontal/quote-app.jsx | 9 ++++++--- stories/src/primatives/author-list.jsx | 9 +++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/stories/src/horizontal/author-app.jsx b/stories/src/horizontal/author-app.jsx index 08022bfc32..a21b914bd8 100644 --- a/stories/src/horizontal/author-app.jsx +++ b/stories/src/horizontal/author-app.jsx @@ -85,6 +85,7 @@ export default class AuthorApp extends Component { > diff --git a/stories/src/multiple-horizontal/quote-app.jsx b/stories/src/multiple-horizontal/quote-app.jsx index 769e61044e..a9e37ca1c1 100644 --- a/stories/src/multiple-horizontal/quote-app.jsx +++ b/stories/src/multiple-horizontal/quote-app.jsx @@ -122,15 +122,18 @@ export default class QuoteApp extends Component { > diff --git a/stories/src/primatives/author-list.jsx b/stories/src/primatives/author-list.jsx index d76268a5be..0f6a178d4d 100644 --- a/stories/src/primatives/author-list.jsx +++ b/stories/src/primatives/author-list.jsx @@ -46,8 +46,8 @@ const Container = styled.div` export default class AuthorList extends Component { props: {| quotes: Quote[], + listId: string, listType?: string, - style?: Object, internalScroll?: boolean, |} @@ -78,15 +78,12 @@ export default class AuthorList extends Component { } render() { - const { listId, listType, internalScroll, style } = this.props; + const { listId, listType, internalScroll } = this.props; return ( {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( - + {internalScroll ? ( {this.renderBoard(dropProvided)} From c053acda50df7346244ac8959510f47aa9cc90a5 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 10:13:53 +1000 Subject: [PATCH 110/117] fixing keyboard drop into empty list --- .../move-to-new-droppable/to-foreign-list.js | 3 +- src/types.js | 2 +- stories/4-board-story.js | 4 +- stories/src/board/board.jsx | 9 ++- stories/src/primatives/column.jsx | 4 +- stories/src/primatives/quote-item.jsx | 14 ++++ stories/src/primatives/quote-list.jsx | 4 +- stories/src/vertical/quote-list.jsx | 66 ------------------- .../move-to-new-droppable.spec.js | 2 +- 9 files changed, 34 insertions(+), 74 deletions(-) delete mode 100644 stories/src/vertical/quote-list.jsx 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 54aa07ba89..18aaa3d2b9 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 @@ -38,7 +38,7 @@ export default ({ // based on the axis of the destination const newCenter: Position = moveToEdge({ - source: draggable.page.withMargin, + source: draggable.page.withoutMargin, sourceEdge: 'start', destination: droppable.page.withMargin, destinationEdge: 'start', @@ -75,6 +75,7 @@ export default ({ } const newCenter: Position = moveToEdge({ + // Aligning to visible top of draggable source: draggable.page.withoutMargin, sourceEdge: 'start', destination: target.page.withMargin, diff --git a/src/types.js b/src/types.js index 34c965cfa9..a21660c7d7 100644 --- a/src/types.js +++ b/src/types.js @@ -172,7 +172,7 @@ export type DropResult = {| type: TypeId, source: DraggableLocation, // may not have any destination (drag to nowhere) - destination: ?DraggableLocation + destination: ?DraggableLocation, |} export type DragState = {| diff --git a/stories/4-board-story.js b/stories/4-board-story.js index 87767f519e..4fd707af65 100644 --- a/stories/4-board-story.js +++ b/stories/4-board-story.js @@ -10,9 +10,9 @@ const inProgress: string = 'In progress'; const done: string = 'Done'; const columns: QuoteMap = { - [todo]: getQuotes(0), + [todo]: getQuotes(7), [inProgress]: getQuotes(3), - [done]: getQuotes(4), + [done]: getQuotes(0), }; storiesOf('board', module) diff --git a/stories/src/board/board.jsx b/stories/src/board/board.jsx index a3cf8c92db..72260f7200 100644 --- a/stories/src/board/board.jsx +++ b/stories/src/board/board.jsx @@ -35,6 +35,7 @@ type Props = {| type State = {| columns: QuoteMap, ordered: string[], + lastMovedQuoteId: ?string, |} export default class Board extends Component { @@ -45,6 +46,7 @@ export default class Board extends Component { state: State = { columns: this.props.initial, ordered: Object.keys(this.props.initial), + lastMovedQuoteId: null, } /* eslint-enable react/sort-comp */ @@ -62,6 +64,10 @@ export default class Board extends Component { publishOnDragStart(initial); // $ExpectError - body wont be null document.body.classList.add(isDraggingClassName); + + this.setState({ + lastMovedQuoteId: null, + }); } onDragEnd = (result: DropResult) => { @@ -93,7 +99,6 @@ export default class Board extends Component { } const current: Quote[] = [...this.state.columns[source.droppableId]]; - console.log('current', current); // reordering within the same column if (source.droppableId === destination.droppableId) { @@ -128,6 +133,7 @@ export default class Board extends Component { this.setState({ columns, + lastMovedQuoteId: source.droppableId, }); } @@ -152,6 +158,7 @@ export default class Board extends Component { key={key} title={key} quotes={columns[key]} + autoFocusQuoteId={this.state.lastMovedQuoteId} /> ))} diff --git a/stories/src/primatives/column.jsx b/stories/src/primatives/column.jsx index ba7c92001c..3010987985 100644 --- a/stories/src/primatives/column.jsx +++ b/stories/src/primatives/column.jsx @@ -43,7 +43,8 @@ const Title = styled.h4` export default class Column extends Component { props: {| title: string, - quotes: Quote[] + quotes: Quote[], + autoFocusQuoteId: ?string, |} render() { @@ -69,6 +70,7 @@ export default class Column extends Component { listId={title} listType="QUOTE" quotes={quotes} + autoFocusQuoteId={this.props.autoFocusQuoteId} /> {provided.placeholder} diff --git a/stories/src/primatives/quote-item.jsx b/stories/src/primatives/quote-item.jsx index 3d456dcebe..defcf772f3 100644 --- a/stories/src/primatives/quote-item.jsx +++ b/stories/src/primatives/quote-item.jsx @@ -1,5 +1,6 @@ // @flow import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; import styled from 'styled-components'; import { borderRadius, colors, grid } from '../constants'; import type { Quote } from '../types'; @@ -9,8 +10,11 @@ type Props = { quote: Quote, isDragging: boolean, provided: DraggableProvided, + autoFocus?: boolean, } +type HTMLElement = any; + const Container = styled.a` border-radius: ${borderRadius}px; border: 1px solid grey; @@ -90,6 +94,16 @@ flex-grow: 1; export default class QuoteItem extends Component { props: Props + componentDidMount() { + if (!this.props.autoFocus) { + return; + } + + // eslint-disable-next-line react/no-find-dom-node + const node: HTMLElement = ReactDOM.findDOMNode(this); + node.focus(); + } + render() { const { quote, isDragging, provided } = this.props; diff --git a/stories/src/primatives/quote-list.jsx b/stories/src/primatives/quote-list.jsx index 48db3c21d4..0cf94b75f6 100644 --- a/stories/src/primatives/quote-list.jsx +++ b/stories/src/primatives/quote-list.jsx @@ -41,7 +41,8 @@ export default class QuoteList extends Component { internalScroll?: boolean, isDropDisabled?: boolean, quotes: Quote[], - style?: Object, + // may not be provided - and might be null + autoFocusQuoteId?: ?string, |} renderQuotes = (dropProvided: DroppableProvided) => { @@ -58,6 +59,7 @@ export default class QuoteList extends Component { quote={quote} isDragging={dragSnapshot.isDragging} provided={dragProvided} + autoFocus={Boolean(this.props.autoFocusQuoteId)} /> {dragProvided.placeholder}
diff --git a/stories/src/vertical/quote-list.jsx b/stories/src/vertical/quote-list.jsx deleted file mode 100644 index c69c176e51..0000000000 --- a/stories/src/vertical/quote-list.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import styled from 'styled-components'; -import QuoteItem from '../primatives/quote-item'; -import { grid, colors } from '../constants'; -import { Droppable, Draggable } from '../../../src'; -import type { - DraggableProvided, - DraggableStateSnapshot, - DroppableProvided, - DroppableStateSnapshot, -} from '../../../src/'; -import type { Quote } from '../types'; - -const Container = styled.div` - background-color: ${({ isDraggingOver }) => (isDraggingOver ? colors.blue.lighter : colors.blue.light)}; - display: flex; - flex-direction: column; - padding: ${grid}px; - padding-bottom: 0; - user-select: none; - transition: background-color 0.1s ease; - width: 250px; - min-height: 250px; -`; - -export default class QuoteList extends Component { - props: {| - listId: string, - quotes: Quote[], - listType?: string, - style?: Object, - |} - - render() { - const { listId, listType, style, quotes } = this.props; - return ( - - {(dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot) => ( - - {quotes.map((quote: Quote) => ( - - {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( -
- - {dragProvided.placeholder} -
- )} -
- ))} - {dropProvided.placeholder} -
- )} -
- ); - } -} 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 54ba6020a3..96be2ae5f6 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 @@ -400,7 +400,7 @@ describe('move to new droppable', () => { it('should move to the start edge of the droppable', () => { const expected: Position = moveToEdge({ - source: inHome1.page.withMargin, + source: inHome1.page.withoutMargin, sourceEdge: 'start', destination: foreign.page.withMargin, destinationEdge: 'start', From 78029bcc4dacf0efa6919c592855c4e212980e7e Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 11:44:02 +1000 Subject: [PATCH 111/117] major story refactoring --- stories/3-complex-vertical-list-story.js | 6 +- stories/4-board-story.js | 15 +-- stories/5-multiple-vertical-lists-story.js | 23 ++-- stories/6-multiple-horizontal-lists-story.js | 20 ++-- stories/src/board/board.jsx | 55 +++------- stories/src/{primatives => board}/column.jsx | 11 +- stories/src/data.js | 12 +-- stories/src/multiple-horizontal/quote-app.jsx | 89 ++++----------- stories/src/multiple-vertical/quote-app.jsx | 102 +++++++----------- stories/src/primatives/author-item.jsx | 14 +++ stories/src/primatives/author-list.jsx | 2 + stories/src/primatives/quote-list.jsx | 49 +++++---- stories/src/primatives/title.jsx | 11 ++ stories/src/reorder.js | 81 ++++++++------ stories/src/types.js | 5 - stories/src/vertical-grouped/quote-app.jsx | 40 +++---- stories/src/vertical-nested/quote-list.jsx | 5 +- 17 files changed, 235 insertions(+), 305 deletions(-) rename stories/src/{primatives => board}/column.jsx (91%) create mode 100644 stories/src/primatives/title.jsx diff --git a/stories/3-complex-vertical-list-story.js b/stories/3-complex-vertical-list-story.js index cb3310afef..78d542d421 100644 --- a/stories/3-complex-vertical-list-story.js +++ b/stories/3-complex-vertical-list-story.js @@ -3,13 +3,11 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import NestedQuoteApp from './src/vertical-nested/quote-app'; import GroupedQuoteApp from './src/vertical-grouped/quote-app'; -import { authorWithQuotes } from './src/data'; +import { authorQuoteMap } from './src/data'; storiesOf('complex vertical list', module) .add('grouped', () => ( - + )) // this is kind of strange - but hey, if you want to! .add('nested vertical lists', () => ( diff --git a/stories/4-board-story.js b/stories/4-board-story.js index 4fd707af65..51f1e3679d 100644 --- a/stories/4-board-story.js +++ b/stories/4-board-story.js @@ -2,20 +2,9 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import Board from './src/board/board'; -import { getQuotes } from './src/data'; -import type { QuoteMap } from './src/types'; - -const todo: string = 'Todo'; -const inProgress: string = 'In progress'; -const done: string = 'Done'; - -const columns: QuoteMap = { - [todo]: getQuotes(7), - [inProgress]: getQuotes(3), - [done]: getQuotes(0), -}; +import { authorQuoteMap } from './src/data'; storiesOf('board', module) .add('task board', () => ( - + )); diff --git a/stories/5-multiple-vertical-lists-story.js b/stories/5-multiple-vertical-lists-story.js index fcf20f92c3..f5a3eff09c 100644 --- a/stories/5-multiple-vertical-lists-story.js +++ b/stories/5-multiple-vertical-lists-story.js @@ -3,22 +3,21 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import QuoteApp from './src/multiple-vertical/quote-app'; import { getQuotes } from './src/data'; +import type { QuoteMap } from './src/types'; -const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( - quote => ({ - ...quote, - id: `${namespace}::${quote.id}`, - }) -); +const alpha: string = 'alpha'; +const beta: string = 'beta'; +const gamma: string = 'gamma'; +const delta: string = 'delta'; -const initialQuotes = { - alpha: namespaceQuoteIds(getQuotes(20), 'alpha'), - beta: namespaceQuoteIds(getQuotes(3), 'beta'), - gamma: namespaceQuoteIds(getQuotes(20), 'gamma'), - delta: namespaceQuoteIds(getQuotes(0), 'delta'), +const quoteMap: QuoteMap = { + [alpha]: getQuotes(20), + [beta]: getQuotes(3), + [gamma]: getQuotes(20), + [delta]: getQuotes(0), }; storiesOf('multiple vertical lists', module) .add('stress test', () => ( - + )); diff --git a/stories/6-multiple-horizontal-lists-story.js b/stories/6-multiple-horizontal-lists-story.js index 26dee7ce93..4346efd141 100644 --- a/stories/6-multiple-horizontal-lists-story.js +++ b/stories/6-multiple-horizontal-lists-story.js @@ -3,21 +3,19 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import QuoteApp from './src/multiple-horizontal/quote-app'; import { getQuotes } from './src/data'; +import type { QuoteMap } from './src/types'; -const namespaceQuoteIds = (quoteList, namespace) => quoteList.map( - quote => ({ - ...quote, - id: `${namespace}::${quote.id}`, - }) -); +const alpha: string = 'alpha'; +const beta: string = 'beta'; +const gamma: string = 'gamma'; -const initialQuotes = { - alpha: namespaceQuoteIds(getQuotes(20), 'alpha'), - beta: namespaceQuoteIds(getQuotes(18), 'beta'), - gamma: namespaceQuoteIds(getQuotes(22), 'gamma'), +const quoteMap: QuoteMap = { + [alpha]: getQuotes(20), + [beta]: getQuotes(18), + [gamma]: getQuotes(22), }; storiesOf('multiple horizontal lists', module) .add('stress test', () => ( - + )); diff --git a/stories/src/board/board.jsx b/stories/src/board/board.jsx index 72260f7200..6cef129fb6 100644 --- a/stories/src/board/board.jsx +++ b/stories/src/board/board.jsx @@ -2,9 +2,9 @@ import React, { Component } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { action } from '@storybook/addon-actions'; -import Column from '../primatives/column'; +import Column from './column'; import { colors } from '../constants'; -import reorder from '../reorder'; +import reorder, { reorderQuoteMap } from '../reorder'; import { DragDropContext, Droppable } from '../../../src/'; import type { DropResult, @@ -12,7 +12,7 @@ import type { DraggableLocation, DroppableProvided, } from '../../../src/'; -import type { Quote, QuoteMap } from '../types'; +import type { QuoteMap } from '../types'; const isDraggingClassName = 'is-dragging'; @@ -35,7 +35,7 @@ type Props = {| type State = {| columns: QuoteMap, ordered: string[], - lastMovedQuoteId: ?string, + autoFocusQuoteId: ?string, |} export default class Board extends Component { @@ -46,7 +46,7 @@ export default class Board extends Component { state: State = { columns: this.props.initial, ordered: Object.keys(this.props.initial), - lastMovedQuoteId: null, + autoFocusQuoteId: null, } /* eslint-enable react/sort-comp */ @@ -66,7 +66,7 @@ export default class Board extends Component { document.body.classList.add(isDraggingClassName); this.setState({ - lastMovedQuoteId: null, + autoFocusQuoteId: null, }); } @@ -98,42 +98,15 @@ export default class Board extends Component { return; } - const current: Quote[] = [...this.state.columns[source.droppableId]]; - - // reordering within the same column - if (source.droppableId === destination.droppableId) { - const reordered: Quote[] = reorder( - current, - source.index, - destination.index, - ); - const columns: QuoteMap = { - ...this.state.columns, - [source.droppableId]: reordered, - }; - this.setState({ - columns, - }); - return; - } - - const target: Quote = current[source.index]; - const next: Quote[] = [...this.state.columns[destination.droppableId]]; - - // remove from original - current.splice(source.index, 1); - // insert into next - next.splice(destination.index, 0, target); - - const columns: QuoteMap = { - ...this.state.columns, - [source.droppableId]: current, - [destination.droppableId]: next, - }; + const data = reorderQuoteMap({ + quoteMap: this.state.columns, + source, + destination, + }); this.setState({ - columns, - lastMovedQuoteId: source.droppableId, + columns: data.quoteMap, + autoFocusQuoteId: data.autoFocusQuoteId, }); } @@ -158,7 +131,7 @@ export default class Board extends Component { key={key} title={key} quotes={columns[key]} - autoFocusQuoteId={this.state.lastMovedQuoteId} + autoFocusQuoteId={this.state.autoFocusQuoteId} /> ))} diff --git a/stories/src/primatives/column.jsx b/stories/src/board/column.jsx similarity index 91% rename from stories/src/primatives/column.jsx rename to stories/src/board/column.jsx index 3010987985..05c4fd8497 100644 --- a/stories/src/primatives/column.jsx +++ b/stories/src/board/column.jsx @@ -4,7 +4,8 @@ import styled from 'styled-components'; import { grid, colors, borderRadius } from '../constants'; import { Draggable } from '../../../src/'; import type { DraggableProvided, DraggableStateSnapshot } from '../../../src/'; -import QuoteList from './quote-list'; +import QuoteList from '../primatives/quote-list'; +import Title from '../primatives/title'; import type { Quote } from '../types'; const Wrapper = styled.div` @@ -32,14 +33,6 @@ const Header = styled.div` } `; -const Title = styled.h4` - padding: ${grid}px; - cursor: grab; - transition: background-color ease 0.2s; - flex-grow: 1; - user-select: none; -`; - export default class Column extends Component { props: {| title: string, diff --git a/stories/src/data.js b/stories/src/data.js index 6c7670ff43..96a6964198 100644 --- a/stories/src/data.js +++ b/stories/src/data.js @@ -1,5 +1,5 @@ // @flow -import type { Author, Quote, AuthorWithQuotes } from './types'; +import type { Author, Quote, QuoteMap } from './types'; const jake: Author = { id: '1', @@ -128,8 +128,8 @@ export const getAuthors = (count: number): Author[] => const getByAuthor = (author: Author, items: Quote[]): Quote[] => items.filter((quote: Quote) => quote.author === author); -export const authorWithQuotes: AuthorWithQuotes[] = - authors.map((author: Author): AuthorWithQuotes => ({ - author, - quotes: getByAuthor(author, quotes), - })); +export const authorQuoteMap: QuoteMap = + authors.reduce((previous: QuoteMap, author: Author) => ({ + ...previous, + [author.name]: getByAuthor(author, quotes), + }), {}); diff --git a/stories/src/multiple-horizontal/quote-app.jsx b/stories/src/multiple-horizontal/quote-app.jsx index a9e37ca1c1..0807b0536f 100644 --- a/stories/src/multiple-horizontal/quote-app.jsx +++ b/stories/src/multiple-horizontal/quote-app.jsx @@ -5,9 +5,10 @@ import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import AuthorList from '../primatives/author-list'; import { colors, grid } from '../constants'; -import reorder from '../reorder'; -import type { Quote } from '../types'; -import type { DropResult, DragStart, DraggableLocation } from '../../../src/types'; +import { reorderQuoteMap } from '../reorder'; +import type { ReorderQuoteMapResult } from '../reorder'; +import type { QuoteMap } from '../types'; +import type { DropResult, DragStart } from '../../../src/types'; const publishOnDragStart = action('onDragStart'); const publishOnDragEnd = action('onDragEnd'); @@ -25,51 +26,11 @@ const Root = styled.div` const isDraggingClassName = 'is-dragging'; -/* eslint-disable react/no-unused-prop-types */ -type GroupedQuotes = { - alpha: Quote[], - beta: Quote[], - gamma: Quote[], -} - type Props = {| - initial: GroupedQuotes, -|} - -type State = {| - quotes: GroupedQuotes, + initial: QuoteMap, |} -const resolveDrop = (quotes: GroupedQuotes, - source: DraggableLocation, - destination: DraggableLocation -): GroupedQuotes => { - const newQuotes: GroupedQuotes = { ...quotes }; - - const movedQuote = quotes[source.droppableId][source.index]; - - Object.keys(newQuotes).forEach((listId: string) => { - const list: Quote[] = (() => { - const previous: Quote[] = newQuotes[listId]; - - // moving within the same list - if (listId === source.droppableId) { - return reorder(previous, source.index, destination.index); - } - - // moving to new list - return [ - ...previous.slice(0, destination.index), - movedQuote, - ...previous.slice(destination.index), - ]; - })(); - - newQuotes[listId] = list; - }); - - return newQuotes; -}; +type State = ReorderQuoteMapResult; export default class QuoteApp extends Component { /* eslint-disable react/sort-comp */ @@ -77,7 +38,8 @@ export default class QuoteApp extends Component { state: State state: State = { - quotes: this.props.initial, + quoteMap: this.props.initial, + autoFocusQuoteId: null, }; /* eslint-enable react/sort-comp */ @@ -97,9 +59,11 @@ export default class QuoteApp extends Component { return; } - const quotes = resolveDrop(this.state.quotes, result.source, result.destination); - - this.setState({ quotes }); + this.setState(reorderQuoteMap({ + quoteMap: this.state.quoteMap, + source: result.source, + destination: result.destination, + })); } componentDidMount() { @@ -113,7 +77,7 @@ export default class QuoteApp extends Component { } render() { - const { quotes } = this.state; + const { quoteMap, autoFocusQuoteId } = this.state; return ( - - - + {Object.keys(quoteMap).map((key: string) => ( + + ))} ); diff --git a/stories/src/multiple-vertical/quote-app.jsx b/stories/src/multiple-vertical/quote-app.jsx index 5ba98eec3f..190d3dc7a0 100644 --- a/stories/src/multiple-vertical/quote-app.jsx +++ b/stories/src/multiple-vertical/quote-app.jsx @@ -5,8 +5,9 @@ import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; import QuoteList from '../primatives/quote-list'; import { colors, grid } from '../constants'; -import reorder from '../reorder'; -import type { Quote } from '../types'; +import { reorderQuoteMap } from '../reorder'; +import type { ReorderQuoteMapResult } from '../reorder'; +import type { QuoteMap } from '../types'; import type { DropResult, DragStart, DraggableLocation } from '../../../src/types'; const publishOnDragStart = action('onDragStart'); @@ -34,53 +35,11 @@ const PushDown = styled.div` const isDraggingClassName = 'is-dragging'; -/* eslint-disable react/no-unused-prop-types */ -type GroupedQuotes = { - alpha: Quote[], - beta: Quote[], - gamma: Quote[], - delta: Quote[], -} - type Props = {| - initial: GroupedQuotes, -|} - -type State = {| - disabledDroppable: ?string, - quotes: GroupedQuotes, + initial: QuoteMap, |} -const resolveDrop = (quotes: GroupedQuotes, - source: DraggableLocation, - destination: DraggableLocation -): GroupedQuotes => { - const newQuotes: GroupedQuotes = { ...quotes }; - - const movedQuote = quotes[source.droppableId][source.index]; - - Object.keys(newQuotes).forEach((listId: string) => { - const list: Quote[] = (() => { - const previous: Quote[] = newQuotes[listId]; - - // moving within the same list - if (listId === source.droppableId) { - return reorder(previous, source.index, destination.index); - } - - // moving to new list - return [ - ...previous.slice(0, destination.index), - movedQuote, - ...previous.slice(destination.index), - ]; - })(); - - newQuotes[listId] = list; - }); - - return newQuotes; -}; +type State = ReorderQuoteMapResult export default class QuoteApp extends Component { /* eslint-disable react/sort-comp */ @@ -88,16 +47,16 @@ export default class QuoteApp extends Component { state: State state: State = { - disabledDroppable: null, - quotes: this.props.initial, + quoteMap: this.props.initial, + autoFocusQuoteId: null, }; /* eslint-enable react/sort-comp */ onDragStart = (initial: DragStart) => { publishOnDragStart(initial); - this.setState({ - disabledDroppable: this.getDisabledDroppable(initial.source.droppableId), - }); + // this.setState({ + // disabledDroppable: this.getDisabledDroppable(initial.source.droppableId), + // }); // $ExpectError - body could be null? document.body.classList.add(isDraggingClassName); } @@ -107,16 +66,19 @@ export default class QuoteApp extends Component { // $ExpectError - body could be null? document.body.classList.remove(isDraggingClassName); - const quotes = result.destination ? ( - resolveDrop(this.state.quotes, result.source, result.destination) - ) : ( - this.state.quotes - ); + // dropped nowhere + if (!result.destination) { + return; + } + + const source: DraggableLocation = result.source; + const destination: DraggableLocation = result.destination; - this.setState({ - disabledDroppable: null, - quotes, - }); + this.setState(reorderQuoteMap({ + quoteMap: this.state.quoteMap, + source, + destination, + })); } componentDidMount() { @@ -129,6 +91,7 @@ export default class QuoteApp extends Component { `; } + // TODO getDisabledDroppable = (sourceDroppable: ?string) => { if (!sourceDroppable) { return null; @@ -142,7 +105,8 @@ export default class QuoteApp extends Component { } render() { - const { quotes, disabledDroppable } = this.state; + const { quoteMap, autoFocusQuoteId } = this.state; + const disabledDroppable = 'TODO'; return ( diff --git a/stories/src/primatives/author-item.jsx b/stories/src/primatives/author-item.jsx index 36391dc938..c826a25e25 100644 --- a/stories/src/primatives/author-item.jsx +++ b/stories/src/primatives/author-item.jsx @@ -1,10 +1,13 @@ // @flow import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; import styled from 'styled-components'; import { colors, grid } from '../constants'; import type { DraggableProvided, DraggableStateSnapshot } from '../../../src/'; import type { Author } from '../types'; +type HTMLElement = any; + const Avatar = styled.img` width: 60px; height: 60px; @@ -30,8 +33,19 @@ export default class AuthorItem extends Component { author: Author, provided: DraggableProvided, snapshot: DraggableStateSnapshot, + autoFocus?: boolean, |} + componentDidMount() { + if (!this.props.autoFocus) { + return; + } + + // eslint-disable-next-line react/no-find-dom-node + const node: HTMLElement = ReactDOM.findDOMNode(this); + node.focus(); + } + render() { const author: Author = this.props.author; const provided: DraggableProvided = this.props.provided; diff --git a/stories/src/primatives/author-list.jsx b/stories/src/primatives/author-list.jsx index 0f6a178d4d..f05306f26a 100644 --- a/stories/src/primatives/author-list.jsx +++ b/stories/src/primatives/author-list.jsx @@ -49,6 +49,7 @@ export default class AuthorList extends Component { listId: string, listType?: string, internalScroll?: boolean, + autoFocusQuoteId?: ?string, |} renderBoard = (dropProvided: DroppableProvided) => { @@ -65,6 +66,7 @@ export default class AuthorList extends Component { author={quote.author} provided={dragProvided} snapshot={dragSnapshot} + autoFocus={this.props.autoFocusQuoteId === quote.id} /> {dragProvided.placeholder}
diff --git a/stories/src/primatives/quote-list.jsx b/stories/src/primatives/quote-list.jsx index 0cf94b75f6..58f3f1adfd 100644 --- a/stories/src/primatives/quote-list.jsx +++ b/stories/src/primatives/quote-list.jsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { Droppable, Draggable } from '../../../src'; import QuoteItem from '../primatives/quote-item'; import { grid, colors } from '../constants'; +import Title from '../primatives/title'; import type { Quote } from '../types'; import type { DroppableProvided, @@ -24,7 +25,7 @@ const Wrapper = styled.div` width: 250px; `; -const Container = styled.div` +const DropZone = styled.div` /* stop the list collapsing when empty */ min-height: 250px; `; @@ -34,39 +35,49 @@ const ScrollContainer = styled.div` max-height: 800px; `; +const Container = styled.div``; + export default class QuoteList extends Component { props: {| listId: string, + quotes: Quote[], + title?: string, listType?: string, internalScroll?: boolean, - isDropDisabled?: boolean, - quotes: Quote[], + isDropDisabled ?: boolean, + style?: Object, // may not be provided - and might be null autoFocusQuoteId?: ?string, |} renderQuotes = (dropProvided: DroppableProvided) => { const { listType, quotes } = this.props; + const title = this.props.title ? ( + {this.props.title} + ) : null; return ( - - {quotes.map((quote: Quote) => ( - - {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( -
- - {dragProvided.placeholder} -
+ + {title} + + {quotes.map((quote: Quote) => ( + + {(dragProvided: DraggableProvided, dragSnapshot: DraggableStateSnapshot) => ( +
+ + {dragProvided.placeholder} +
)} -
+
))} - {dropProvided.placeholder} + {dropProvided.placeholder} +
); } diff --git a/stories/src/primatives/title.jsx b/stories/src/primatives/title.jsx new file mode 100644 index 0000000000..751e41e662 --- /dev/null +++ b/stories/src/primatives/title.jsx @@ -0,0 +1,11 @@ +// @flow +import styled from 'styled-components'; +import { 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/reorder.js b/stories/src/reorder.js index 9e6faec1be..7aaf29e896 100644 --- a/stories/src/reorder.js +++ b/stories/src/reorder.js @@ -1,6 +1,6 @@ // @flow -import type { AuthorWithQuotes } from './types'; -import type { DropResult, DraggableLocation } from '../../src/types'; +import type { Quote, QuoteMap } from './types'; +import type { DraggableLocation } from '../../src/types'; // a little function to help us with reordering the result const reorder = ( @@ -16,39 +16,60 @@ const reorder = ( export default reorder; -export const reorderGroup = ( - groups: AuthorWithQuotes[], - result: DropResult -): ?AuthorWithQuotes[] => { - if (!result.destination) { - return null; - } +type ReorderQuoteMapArgs = {| + quoteMap: QuoteMap, + source: DraggableLocation, + destination: DraggableLocation, +|} - const source: DraggableLocation = result.source; - const destination: DraggableLocation = result.destination; +export type ReorderQuoteMapResult = {| + quoteMap: QuoteMap, + autoFocusQuoteId: ?string, +|} - const group: ?AuthorWithQuotes = groups.filter( - (item: AuthorWithQuotes) => item.author.id === result.type - )[0]; +export const reorderQuoteMap = ({ + quoteMap, + source, + destination, +}: ReorderQuoteMapArgs): ReorderQuoteMapResult => { + const current: Quote[] = [...quoteMap[source.droppableId]]; + const next: Quote[] = [...quoteMap[destination.droppableId]]; + const target: Quote = current[source.index]; - if (!group) { - console.error('could not find group', result.type, groups); - return null; + // moving to same list + if (source.droppableId === destination.droppableId) { + const reordered: Quote[] = reorder( + current, + source.index, + destination.index, + ); + const result: QuoteMap = { + ...quoteMap, + [source.droppableId]: reordered, + }; + return { + quoteMap: result, + // not auto focusing in own list + autoFocusQuoteId: null, + }; } - const quotes = reorder( - group.quotes, - source.index, - destination.index - ); + // moving to different list - const updated: AuthorWithQuotes = { - author: group.author, - quotes, - }; + // remove from original + current.splice(source.index, 1); + // insert into next + next.splice(destination.index, 0, target); - const newGroups: AuthorWithQuotes[] = Array.from(groups); - newGroups[groups.indexOf(group)] = updated; + const result: QuoteMap = { + ...quoteMap, + [source.droppableId]: current, + [destination.droppableId]: next, + }; - return newGroups; -}; + return { + quoteMap: result, + autoFocusQuoteId: target.id, + }; +} +; diff --git a/stories/src/types.js b/stories/src/types.js index bbe3ccf8a8..c9612b5f35 100644 --- a/stories/src/types.js +++ b/stories/src/types.js @@ -19,11 +19,6 @@ export type Dragging = {| location: DraggableLocation, |} -export type AuthorWithQuotes = {| - author: Author, - quotes: Quote[], -|} - export type QuoteMap = { [key: string]: Quote[] } diff --git a/stories/src/vertical-grouped/quote-app.jsx b/stories/src/vertical-grouped/quote-app.jsx index 1c6ddaccf6..d520365a92 100644 --- a/stories/src/vertical-grouped/quote-app.jsx +++ b/stories/src/vertical-grouped/quote-app.jsx @@ -3,10 +3,10 @@ import React, { Component } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src/'; -import QuoteList from '../vertical/quote-list'; +import QuoteList from '../primatives/quote-list'; import { colors, grid } from '../constants'; -import { reorderGroup } from '../reorder'; -import type { AuthorWithQuotes } from '../types'; +import { reorderQuoteMap } from '../reorder'; +import type { QuoteMap } from '../types'; import type { DropResult, DragStart } from '../../../src/types'; const publishOnDragStart = action('onDragStart'); @@ -40,11 +40,11 @@ const Title = styled.h4` const isDraggingClassName = 'is-dragging'; type Props = {| - initial: AuthorWithQuotes[], + initial: QuoteMap, |} type State = {| - groups: AuthorWithQuotes[], + quoteMap: QuoteMap, |} export default class QuoteApp extends Component { @@ -53,7 +53,7 @@ export default class QuoteApp extends Component { state: State state: State = { - groups: this.props.initial, + quoteMap: this.props.initial, }; /* eslint-enable react/sort-comp */ @@ -68,17 +68,17 @@ export default class QuoteApp extends Component { // $ExpectError - body could be null? document.body.classList.remove(isDraggingClassName); - const groups: ?AuthorWithQuotes[] = reorderGroup( - this.state.groups, result - ); - - if (!groups) { + if (!result.destination) { return; } - this.setState({ - groups, + const { quoteMap } = reorderQuoteMap({ + quoteMap: this.state.quoteMap, + source: result.source, + destination: result.destination, }); + + this.setState({ quoteMap }); } componentDidMount() { @@ -92,7 +92,7 @@ export default class QuoteApp extends Component { } render() { - const { groups } = this.state; + const { quoteMap } = this.state; return ( - {groups.map((group: AuthorWithQuotes) => ( - - {group.author.name} + {Object.keys(quoteMap).map((key: string) => ( + + {key} ))} diff --git a/stories/src/vertical-nested/quote-list.jsx b/stories/src/vertical-nested/quote-list.jsx index a4027c835a..8e62a51c36 100644 --- a/stories/src/vertical-nested/quote-list.jsx +++ b/stories/src/vertical-nested/quote-list.jsx @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import styled from 'styled-components'; import { Droppable, Draggable } from '../../../src'; import QuoteItem from '../primatives/quote-item'; +import Title from '../primatives/title'; import { grid, colors } from '../constants'; import type { Quote } from '../types'; import type { NestedQuoteList } from './types'; @@ -35,10 +36,6 @@ const NestedContainer = Container.extend` } `; -const Title = styled.h4` - margin-bottom: ${grid}px; -`; - export default class QuoteList extends Component { props: {| list: NestedQuoteList From b114b1c1df62ed08ea26047154380eab3b198eb6 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 14:00:10 +1000 Subject: [PATCH 112/117] adding checks for padding --- src/state/dimension.js | 76 +++++++++---------- src/state/get-client-rect.js | 11 +-- src/state/get-new-home-client-center.js | 2 +- src/types.js | 27 +++++-- src/view/dimension-types.js | 15 ---- .../draggable-dimension-publisher.jsx | 6 +- .../droppable-dimension-publisher.jsx | 18 ++++- test/unit/state/dimension.spec.js | 6 +- .../move-to-new-droppable.spec.js | 4 +- ...cted-draggable-dimension-publisher.spec.js | 6 +- ...cted-droppable-dimension-publisher.spec.js | 6 +- test/utils/get-droppable-with-draggables.js | 12 +-- test/utils/get-fragment.js | 3 +- 13 files changed, 91 insertions(+), 101 deletions(-) delete mode 100644 src/view/dimension-types.js diff --git a/src/state/dimension.js b/src/state/dimension.js index bf1495a331..ec84a03128 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -1,5 +1,6 @@ // @flow import { vertical, horizontal } from './axis'; +import getClientRect from './get-client-rect'; import type { DroppableId, DraggableId, @@ -8,27 +9,13 @@ import type { DroppableDimension, Direction, DimensionFragment, + Spacing, + ClientRect, } from '../types'; -export type ClientRect = {| - top: number, - right: number, - bottom: number, - left: number, - width: number, - height: number, -|} - -export type Margin = {| - top: number, - right: number, - bottom: number, - left: number, -|} - const origin: Position = { x: 0, y: 0 }; -export const noMargin: Margin = { +export const noSpacing: Spacing = { top: 0, right: 0, bottom: 0, @@ -36,27 +23,23 @@ export const noMargin: Margin = { }; const getWithPosition = (clientRect: ClientRect, point: Position): ClientRect => { - const { top, right, bottom, left, width, height } = clientRect; - return { + const { top, right, bottom, left } = clientRect; + return getClientRect({ top: top + point.y, left: left + point.x, bottom: bottom + point.y, right: right + point.x, - height, - width, - }; + }); }; -const getWithMargin = (clientRect: ClientRect, margin: Margin): ClientRect => { - const { top, right, bottom, left, height, width } = clientRect; - return { - top: top + margin.top, - left: left + margin.left, - bottom: bottom + margin.bottom, - right: right + margin.right, - height: height + margin.top + margin.bottom, - width: width + margin.left + margin.right, - }; +const getWithSpacing = (clientRect: ClientRect, spacing: Spacing): ClientRect => { + const { top, right, bottom, left } = clientRect; + return getClientRect({ + top: top + spacing.top, + left: left + spacing.left, + bottom: bottom + spacing.bottom, + right: right + spacing.right, + }); }; const getFragment = ( @@ -79,7 +62,7 @@ type GetDraggableArgs = {| id: DraggableId, droppableId: DroppableId, clientRect: ClientRect, - margin?: Margin, + margin?: Spacing, windowScroll?: Position, |}; @@ -87,11 +70,11 @@ export const getDraggableDimension = ({ id, droppableId, clientRect, - margin = noMargin, + margin = noSpacing, windowScroll = origin, }: GetDraggableArgs): DraggableDimension => { const withScroll = getWithPosition(clientRect, windowScroll); - const withScrollAndMargin = getWithMargin(withScroll, margin); + const withScrollAndMargin = getWithSpacing(withScroll, margin); const dimension: DraggableDimension = { id, @@ -99,7 +82,7 @@ export const getDraggableDimension = ({ // on the viewport client: { withoutMargin: getFragment(clientRect), - withMargin: getFragment(getWithMargin(clientRect, margin)), + withMargin: getFragment(getWithSpacing(clientRect, margin)), }, // with scroll page: { @@ -115,7 +98,8 @@ type GetDroppableArgs = {| id: DroppableId, clientRect: ClientRect, direction?: Direction, - margin?: Margin, + margin?: Spacing, + padding?: Spacing, windowScroll?: Position, scroll ?: Position, // Whether or not the droppable is currently enabled (can change at during a drag) @@ -123,17 +107,25 @@ type GetDroppableArgs = {| isEnabled?: boolean, |} +const add = (spacing1: Spacing, spacing2: Spacing): Spacing => ({ + top: spacing1.top + spacing2.top, + left: spacing1.left + spacing2.left, + right: spacing1.right + spacing2.right, + bottom: spacing1.bottom + spacing2.bottom, +}); + export const getDroppableDimension = ({ id, clientRect, direction = 'vertical', - margin = noMargin, + margin = noSpacing, + padding = noSpacing, windowScroll = origin, scroll = origin, isEnabled = true, }: GetDroppableArgs): DroppableDimension => { + const withMargin = getWithSpacing(clientRect, margin); const withWindowScroll = getWithPosition(clientRect, windowScroll); - const withWindowScrollAndMargin = getWithMargin(withWindowScroll, margin); const dimension: DroppableDimension = { id, @@ -146,11 +138,13 @@ export const getDroppableDimension = ({ }, client: { withoutMargin: getFragment(clientRect), - withMargin: getFragment(getWithMargin(clientRect, margin)), + withMargin: getFragment(withMargin), + withMarginAndPadding: getFragment(getWithSpacing(withMargin, padding)), }, page: { withoutMargin: getFragment(withWindowScroll), - withMargin: getFragment(withWindowScrollAndMargin), + withMargin: getFragment(getWithSpacing(withWindowScroll, margin)), + withMarginAndPadding: getFragment(getWithSpacing(withWindowScroll, add(margin, padding))), }, }; diff --git a/src/state/get-client-rect.js b/src/state/get-client-rect.js index ecf212339f..65bdc423ae 100644 --- a/src/state/get-client-rect.js +++ b/src/state/get-client-rect.js @@ -1,15 +1,8 @@ // @flow -import type { ClientRect } from './dimension'; - -type Args = { - top: number, - right: number, - bottom: number, - left: number, -} +import type { Spacing, ClientRect } from '../types'; // $ExpectError - flow cannot handle using an axis to create these -export default ({ top, right, bottom, left }: Args): ClientRect => ({ +export default ({ top, right, bottom, left }: Spacing): ClientRect => ({ top, right, bottom, diff --git a/src/state/get-new-home-client-center.js b/src/state/get-new-home-client-center.js index feafb1e7a3..d4e4a7090c 100644 --- a/src/state/get-new-home-client-center.js +++ b/src/state/get-new-home-client-center.js @@ -71,7 +71,7 @@ export default ({ // Otherwise, return the dimension of the empty foreign droppable // $ExpectError - flow does not correctly type this as non optional - return destination.client.withMargin; + return destination.client.withMarginAndPadding; })(); const { sourceEdge, destinationEdge } = (() => { diff --git a/src/types.js b/src/types.js index a21660c7d7..d81bd5c17b 100644 --- a/src/types.js +++ b/src/types.js @@ -13,6 +13,22 @@ export type Position = {| y: number, |}; +export type Spacing = {| + top: number, + right: number, + bottom: number, + left: number, +|} + +export type ClientRect = {| + top: number, + right: number, + bottom: number, + left: number, + width: number, + height: number, +|} + export type Direction = 'horizontal' | 'vertical'; export type VerticalAxis = {| @@ -42,12 +58,7 @@ export type HorizontalAxis = {| export type Axis = VerticalAxis | HorizontalAxis export type DimensionFragment = {| - top: number, - left: number, - bottom: number, - right: number, - width: number, - height: number, + ...ClientRect, center: Position, |} @@ -78,11 +89,15 @@ export type DroppableDimension = {| client: {| withMargin: DimensionFragment, withoutMargin: DimensionFragment, + // the area in which content presses up against + withMarginAndPadding: DimensionFragment, |}, // relative to the whole page page: {| withMargin: DimensionFragment, withoutMargin: DimensionFragment, + // the area in which content presses up against + withMarginAndPadding: DimensionFragment, |}, |} export type DraggableLocation = {| diff --git a/src/view/dimension-types.js b/src/view/dimension-types.js deleted file mode 100644 index 926f1eb7d1..0000000000 --- a/src/view/dimension-types.js +++ /dev/null @@ -1,15 +0,0 @@ -export type ClientRect = {| - top: number, - right: number, - bottom: number, - left: number, - width: number, - height: number, -|} - -export type Margin = {| - top: number, - right: number, - bottom: number, - left: number, -|} diff --git a/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx b/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx index 6d425533a2..d0a42dbb43 100644 --- a/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx +++ b/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx @@ -3,9 +3,7 @@ import { Component } from 'react'; import invariant from 'invariant'; import getWindowScrollPosition from '../get-window-scroll-position'; import { getDraggableDimension } from '../../state/dimension'; -// eslint-disable-next-line no-duplicate-imports -import type { Margin } from '../../state/dimension'; -import type { DraggableDimension } from '../../types'; +import type { DraggableDimension, Spacing } from '../../types'; import type { Props } from './draggable-dimension-publisher-types'; export default class DraggableDimensionPublisher extends Component { @@ -23,7 +21,7 @@ export default class DraggableDimensionPublisher extends Component { const style = window.getComputedStyle(targetRef); - const margin: Margin = { + const margin: Spacing = { top: parseInt(style.marginTop, 10), right: parseInt(style.marginRight, 10), bottom: parseInt(style.marginBottom, 10), diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index 13e7ba88b3..4a1a813106 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -8,8 +8,13 @@ import getClientRect from '../../state/get-client-rect'; import { getDroppableDimension } from '../../state/dimension'; import getClosestScrollable from '../get-closest-scrollable'; // eslint-disable-next-line no-duplicate-imports -import type { Margin, ClientRect } from '../../state/dimension'; -import type { DroppableDimension, Position, HTMLElement } from '../../types'; +import type { + DroppableDimension, + Position, + HTMLElement, + ClientRect, + Spacing, +} from '../../types'; import type { Props } from './droppable-dimension-publisher-types'; const origin: Position = { x: 0, y: 0 }; @@ -43,12 +48,18 @@ export default class DroppableDimensionPublisher extends Component { // keeping it simple and always using the margin of the droppable - const margin: Margin = { + const margin: Spacing = { top: parseInt(style.marginTop, 10), right: parseInt(style.marginRight, 10), bottom: parseInt(style.marginBottom, 10), left: parseInt(style.marginLeft, 10), }; + const padding: Spacing = { + top: parseInt(style.paddingTop, 10), + right: parseInt(style.paddingRight, 10), + bottom: parseInt(style.paddingBottom, 10), + left: parseInt(style.paddingLeft, 10), + }; const clientRect: ClientRect = (() => { const current: ClientRect = targetRef.getBoundingClientRect(); @@ -85,6 +96,7 @@ export default class DroppableDimensionPublisher extends Component { direction, clientRect, margin, + padding, windowScroll: getWindowScrollPosition(), scroll, isEnabled: !isDropDisabled, diff --git a/test/unit/state/dimension.spec.js b/test/unit/state/dimension.spec.js index b7586847e3..546d57a698 100644 --- a/test/unit/state/dimension.spec.js +++ b/test/unit/state/dimension.spec.js @@ -4,9 +4,9 @@ import { getDroppableDimension, } from '../../../src/state/dimension'; import { vertical, horizontal } from '../../../src/state/axis'; -// eslint-disable-next-line no-duplicate-imports -import type { Margin, ClientRect } from '../../../src/state/dimension'; import type { + ClientRect, + Spacing, DraggableId, DroppableId, Position, @@ -26,7 +26,7 @@ const clientRect: ClientRect = { width: 90, height: 80, }; -const margin: Margin = { +const margin: Spacing = { top: 1, right: 2, bottom: 3, left: 4, }; const windowScroll: Position = { 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 96be2ae5f6..de4137fe64 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 @@ -6,9 +6,9 @@ import getClientRect from '../../../../src/state/get-client-rect'; import moveToEdge from '../../../../src/state/move-to-edge'; import { patch } from '../../../../src/state/position'; import { horizontal, vertical } from '../../../../src/state/axis'; -import type { Margin } from '../../../../src/state/dimension'; import type { Axis, + Spacing, DragImpact, DraggableDimension, DroppableDimension, @@ -24,7 +24,7 @@ describe('move to new droppable', () => { console.error.mockRestore(); }); - const noMargin: Margin = { top: 0, left: 0, bottom: 0, right: 0 }; + const noMargin: Spacing = { top: 0, left: 0, bottom: 0, right: 0 }; [vertical, horizontal].forEach((axis: Axis) => { describe(`on ${axis.direction} axis`, () => { diff --git a/test/unit/view/unconnected-draggable-dimension-publisher.spec.js b/test/unit/view/unconnected-draggable-dimension-publisher.spec.js index 4b972fcdf5..f4038678c2 100644 --- a/test/unit/view/unconnected-draggable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-draggable-dimension-publisher.spec.js @@ -3,11 +3,11 @@ import React, { Component } from 'react'; import { mount } from 'enzyme'; import DraggableDimensionPublisher from '../../../src/view/draggable-dimension-publisher/draggable-dimension-publisher'; import { getDraggableDimension } from '../../../src/state/dimension'; -// eslint-disable-next-line no-duplicate-imports -import type { ClientRect, Margin } from '../../../src/state/dimension'; import getClientRect from '../../../src/state/get-client-rect'; import setWindowScroll from '../../utils/set-window-scroll'; import type { + Spacing, + ClientRect, Position, DraggableId, DroppableId, @@ -117,7 +117,7 @@ describe('DraggableDimensionPublisher', () => { }); it('should consider any margins when calculating dimensions', () => { - const margin: Margin = { + const margin: Spacing = { top: 10, right: 30, bottom: 40, diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js index 1c7673e280..86ae24a24b 100644 --- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js @@ -4,11 +4,11 @@ import React, { Component } from 'react'; import { mount } from 'enzyme'; import DroppableDimensionPublisher from '../../../src/view/droppable-dimension-publisher/droppable-dimension-publisher'; import { getDroppableDimension } from '../../../src/state/dimension'; -// eslint-disable-next-line no-duplicate-imports -import type { Margin, ClientRect } from '../../../src/state/dimension'; import getClientRect from '../../../src/state/get-client-rect'; import setWindowScroll from '../../utils/set-window-scroll'; import type { + ClientRect, + Spacing, DroppableId, DroppableDimension, HTMLElement, @@ -158,7 +158,7 @@ describe('DraggableDimensionPublisher', () => { }); it('should consider any margins when calculating dimensions', () => { - const margin: Margin = { + const margin: Spacing = { top: 10, right: 30, bottom: 40, diff --git a/test/utils/get-droppable-with-draggables.js b/test/utils/get-droppable-with-draggables.js index 7096cf20e2..d0c809b856 100644 --- a/test/utils/get-droppable-with-draggables.js +++ b/test/utils/get-droppable-with-draggables.js @@ -6,21 +6,15 @@ import type { DraggableDimensionMap, DroppableDimension, Direction, + Spacing, } from '../../src/types'; import getClientRect from '../../src/state/get-client-rect'; -type ClientRectSubset = {| - top: number, - left: number, - right: number, - bottom: number, -|} - type Args = {| direction?: Direction, droppableId?: DroppableId, - droppableRect: ClientRectSubset, - draggableRects: ClientRectSubset[], + droppableRect: Spacing, + draggableRects: Spacing[], |}; export type Result = { diff --git a/test/utils/get-fragment.js b/test/utils/get-fragment.js index 9131272486..eaa7054035 100644 --- a/test/utils/get-fragment.js +++ b/test/utils/get-fragment.js @@ -1,6 +1,5 @@ // @flow -import type { DimensionFragment } from '../../src/types'; -import type { ClientRect } from '../../src/state/dimension'; +import type { DimensionFragment, ClientRect } from '../../src/types'; export default (clientRect: ClientRect): DimensionFragment => { const { top, left, bottom, right, width, height } = clientRect; From cfbd43f3af9f76cb608498b735c3c9a152bf1d43 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 15:00:37 +1000 Subject: [PATCH 113/117] droppable dimension is now padding aware --- src/state/dimension.js | 36 +-- .../integration/hooks-integration.spec.js | 4 + test/unit/state/dimension.spec.js | 218 +++++++++++++----- ...cted-draggable-dimension-publisher.spec.js | 10 +- ...cted-droppable-dimension-publisher.spec.js | 56 ++--- 5 files changed, 213 insertions(+), 111 deletions(-) diff --git a/src/state/dimension.js b/src/state/dimension.js index ec84a03128..7f627c9e4e 100644 --- a/src/state/dimension.js +++ b/src/state/dimension.js @@ -45,18 +45,27 @@ const getWithSpacing = (clientRect: ClientRect, spacing: Spacing): ClientRect => const getFragment = ( initial: ClientRect | DimensionFragment, point?: Position = origin, -): DimensionFragment => ({ - top: initial.top + point.y, - left: initial.left + point.x, - bottom: initial.bottom + point.y, - right: initial.right + point.x, - width: initial.width, - height: initial.height, - center: { - x: ((initial.right + point.x) + (initial.left + point.x)) / 2, - y: ((initial.bottom + point.y) + (initial.top + point.y)) / 2, - }, -}); +): DimensionFragment => { + const rect: ClientRect = getClientRect({ + top: initial.top + point.y, + left: initial.left + point.x, + bottom: initial.bottom + point.y, + right: initial.right + point.x, + }); + + return { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + center: { + x: (rect.right + rect.left) / 2, + y: (rect.bottom + rect.top) / 2, + }, + }; +}; type GetDraggableArgs = {| id: DraggableId, @@ -74,7 +83,6 @@ export const getDraggableDimension = ({ windowScroll = origin, }: GetDraggableArgs): DraggableDimension => { const withScroll = getWithPosition(clientRect, windowScroll); - const withScrollAndMargin = getWithSpacing(withScroll, margin); const dimension: DraggableDimension = { id, @@ -87,7 +95,7 @@ export const getDraggableDimension = ({ // with scroll page: { withoutMargin: getFragment(withScroll), - withMargin: getFragment(withScrollAndMargin), + withMargin: getFragment(getWithSpacing(withScroll, margin)), }, }; diff --git a/test/unit/integration/hooks-integration.spec.js b/test/unit/integration/hooks-integration.spec.js index 1d26b0d9e0..b2ebcec22c 100644 --- a/test/unit/integration/hooks-integration.spec.js +++ b/test/unit/integration/hooks-integration.spec.js @@ -48,6 +48,10 @@ describe('hooks integration', () => { marginRight: '0', marginBottom: '0', marginLeft: '0', + paddingTop: '0', + paddingRight: '0', + paddingBottom: '0', + paddingLeft: '0', })); return mount( diff --git a/test/unit/state/dimension.spec.js b/test/unit/state/dimension.spec.js index 546d57a698..452f645350 100644 --- a/test/unit/state/dimension.spec.js +++ b/test/unit/state/dimension.spec.js @@ -4,6 +4,7 @@ import { getDroppableDimension, } from '../../../src/state/dimension'; import { vertical, horizontal } from '../../../src/state/axis'; +import getClientRect from '../../../src/state/get-client-rect'; import type { ClientRect, Spacing, @@ -29,11 +30,19 @@ const clientRect: ClientRect = { const margin: Spacing = { top: 1, right: 2, bottom: 3, left: 4, }; +const padding: Spacing = { + top: 5, right: 6, bottom: 7, left: 8, +}; const windowScroll: Position = { x: 50, y: 80, }; +const getCenter = (rect: ClientRect): Position => ({ + x: (rect.left + rect.right) / 2, + y: (rect.top + rect.bottom) / 2, +}); + describe('dimension', () => { describe('draggable dimension', () => { const dimension: DraggableDimension = getDraggableDimension({ @@ -44,7 +53,7 @@ describe('dimension', () => { windowScroll, }); - describe('client coordinates', () => { + describe('without scroll (client)', () => { it('should return a portion that does not account for margins', () => { const fragment: DimensionFragment = { top: clientRect.top, @@ -62,23 +71,27 @@ describe('dimension', () => { }); it('should return a portion that considers margins', () => { - const fragment: DimensionFragment = { + const rect: ClientRect = getClientRect({ top: clientRect.top + margin.top, right: clientRect.right + margin.right, bottom: clientRect.bottom + margin.bottom, left: clientRect.left + margin.left, - width: clientRect.width + margin.left + margin.right, - height: clientRect.height + margin.top + margin.bottom, - center: { - x: (clientRect.left + margin.left + clientRect.right + margin.right) / 2, - y: (clientRect.top + margin.top + clientRect.bottom + margin.bottom) / 2, - }, + }); + + const fragment: DimensionFragment = { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + center: getCenter(rect), }; expect(dimension.client.withMargin).toEqual(fragment); }); }); - describe('page coordination', () => { + describe('with scroll (page)', () => { it('should return a portion that does not account for margins', () => { const top: number = clientRect.top + windowScroll.y; const right: number = clientRect.right + windowScroll.x; @@ -101,22 +114,21 @@ describe('dimension', () => { }); it('should return a portion that considers margins', () => { - const top: number = clientRect.top + margin.top + windowScroll.y; - const right: number = clientRect.right + margin.right + windowScroll.x; - const bottom: number = clientRect.bottom + margin.bottom + windowScroll.y; - const left: number = clientRect.left + margin.left + windowScroll.x; + const rect: ClientRect = getClientRect({ + top: clientRect.top + margin.top + windowScroll.y, + right: clientRect.right + margin.right + windowScroll.x, + bottom: clientRect.bottom + margin.bottom + windowScroll.y, + left: clientRect.left + margin.left + windowScroll.x, + }); const fragment: DimensionFragment = { - top, - right, - bottom, - left, - width: clientRect.width + margin.left + margin.right, - height: clientRect.height + margin.top + margin.bottom, - center: { - x: (left + right) / 2, - y: (top + bottom) / 2, - }, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + center: getCenter(rect), }; expect(dimension.page.withMargin).toEqual(fragment); @@ -134,6 +146,7 @@ describe('dimension', () => { id: droppableId, clientRect, margin, + padding, windowScroll, scroll, }); @@ -178,48 +191,127 @@ describe('dimension', () => { expect(withHorizontal.axis).toBe(horizontal); }); - it('should return a portion that does not consider margins', () => { - const top: number = clientRect.top + windowScroll.y; - const left: number = clientRect.left + windowScroll.x; - const bottom: number = clientRect.bottom + windowScroll.y; - const right: number = clientRect.right + windowScroll.x; - - const fragment: DimensionFragment = { - top, - right, - bottom, - left, - width: clientRect.width, - height: clientRect.height, - center: { - x: (left + right) / 2, - y: (top + bottom) / 2, - }, - }; - - expect(dimension.page.withoutMargin).toEqual(fragment); + describe('without scroll (client)', () => { + it('should return a portion that does not consider margins', () => { + const fragment: DimensionFragment = { + top: clientRect.top, + right: clientRect.right, + bottom: clientRect.bottom, + left: clientRect.left, + width: clientRect.width, + height: clientRect.height, + center: getCenter(clientRect), + }; + + expect(dimension.client.withoutMargin).toEqual(fragment); + }); + + it('should return a portion that does consider margins', () => { + const rect: ClientRect = getClientRect({ + top: clientRect.top + margin.top, + left: clientRect.left + margin.left, + bottom: clientRect.bottom + margin.bottom, + right: clientRect.right + margin.right, + }); + + const fragment: DimensionFragment = { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + center: getCenter(rect), + }; + + expect(dimension.client.withMargin).toEqual(fragment); + }); + + it('should return a portion that considers margins and padding', () => { + const rect: ClientRect = getClientRect({ + top: clientRect.top + margin.top + padding.top, + left: clientRect.left + margin.left + padding.left, + bottom: clientRect.bottom + margin.bottom + padding.bottom, + right: clientRect.right + margin.right + padding.right, + }); + + const fragment: DimensionFragment = { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + center: getCenter(rect), + }; + + expect(dimension.client.withMarginAndPadding).toEqual(fragment); + }); }); - it('should return a portion that does consider margins', () => { - const top: number = clientRect.top + windowScroll.y + margin.top; - const left: number = clientRect.left + windowScroll.x + margin.left; - const bottom: number = clientRect.bottom + windowScroll.y + margin.bottom; - const right: number = clientRect.right + windowScroll.x + margin.right; - - const fragment: DimensionFragment = { - top, - right, - bottom, - left, - width: clientRect.width + margin.left + margin.right, - height: clientRect.height + margin.top + margin.bottom, - center: { - x: (left + right) / 2, - y: (top + bottom) / 2, - }, - }; - - expect(dimension.page.withMargin).toEqual(fragment); + describe('with scroll (page)', () => { + it('should return a portion that does not consider margins', () => { + const rect: ClientRect = getClientRect({ + top: clientRect.top + windowScroll.y, + left: clientRect.left + windowScroll.x, + bottom: clientRect.bottom + windowScroll.y, + right: clientRect.right + windowScroll.x, + }); + + const fragment: DimensionFragment = { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + center: getCenter(rect), + }; + + expect(dimension.page.withoutMargin).toEqual(fragment); + }); + + it('should return a portion that does consider margins', () => { + const rect: ClientRect = getClientRect({ + top: clientRect.top + windowScroll.y + margin.top, + left: clientRect.left + windowScroll.x + margin.left, + bottom: clientRect.bottom + windowScroll.y + margin.bottom, + right: clientRect.right + windowScroll.x + margin.right, + }); + + const fragment: DimensionFragment = { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + center: getCenter(rect), + }; + + expect(dimension.page.withMargin).toEqual(fragment); + }); + + it('should return a portion that considers margins and padding', () => { + const rect: ClientRect = getClientRect({ + top: clientRect.top + windowScroll.y + margin.top + padding.top, + left: clientRect.left + windowScroll.x + margin.left + padding.left, + bottom: clientRect.bottom + windowScroll.y + margin.bottom + padding.bottom, + right: clientRect.right + windowScroll.x + margin.right + padding.right, + }); + + const fragment: DimensionFragment = { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height, + center: getCenter(rect), + }; + + expect(dimension.page.withMarginAndPadding).toEqual(fragment); + }); }); }); }); diff --git a/test/unit/view/unconnected-draggable-dimension-publisher.spec.js b/test/unit/view/unconnected-draggable-dimension-publisher.spec.js index f4038678c2..a3930635ab 100644 --- a/test/unit/view/unconnected-draggable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-draggable-dimension-publisher.spec.js @@ -28,11 +28,15 @@ const dimension: DraggableDimension = getDraggableDimension({ }), }); -const noComputedMargin = { +const noSpacing = { marginTop: '0', marginRight: '0', marginBottom: '0', marginLeft: '0', + paddingTop: '0', + paddingRight: '0', + paddingBottom: '0', + paddingLeft: '0', }; class Item extends Component { @@ -105,7 +109,7 @@ describe('DraggableDimensionPublisher', () => { height: dimension.page.withoutMargin.height, width: dimension.page.withoutMargin.width, })); - jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noComputedMargin); + jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing); const wrapper = mount(); wrapper.setProps({ @@ -181,7 +185,7 @@ describe('DraggableDimensionPublisher', () => { windowScroll, }); jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => clientRect); - jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noComputedMargin); + jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing); setWindowScroll(windowScroll); const wrapper = mount(); diff --git a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js index 86ae24a24b..4057ac1f9b 100644 --- a/test/unit/view/unconnected-droppable-dimension-publisher.spec.js +++ b/test/unit/view/unconnected-droppable-dimension-publisher.spec.js @@ -28,6 +28,24 @@ const droppable: DroppableDimension = getDroppableDimension({ }); const origin: Position = { x: 0, y: 0 }; +const noMargin = { + marginTop: '0', + marginRight: '0', + marginBottom: '0', + marginLeft: '0', +}; +const noPadding = { + paddingTop: '0', + paddingRight: '0', + paddingBottom: '0', + paddingLeft: '0', +}; + +const noSpacing = { + ...noMargin, + ...noPadding, +}; + class ScrollableItem extends Component { /* eslint-disable react/sort-comp */ props: { @@ -139,12 +157,7 @@ describe('DraggableDimensionPublisher', () => { height: droppable.page.withoutMargin.height, width: droppable.page.withoutMargin.width, })); - jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({ - marginTop: '0', - marginRight: '0', - marginBottom: '0', - marginLeft: '0', - })); + jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing); wrapper = mount( , @@ -187,6 +200,7 @@ describe('DraggableDimensionPublisher', () => { marginRight: `${margin.right}`, marginBottom: `${margin.bottom}`, marginLeft: `${margin.left}`, + ...noPadding, })); wrapper = mount( @@ -217,12 +231,7 @@ describe('DraggableDimensionPublisher', () => { windowScroll, }); jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => clientRect); - jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({ - marginTop: '0', - marginRight: '0', - marginBottom: '0', - marginLeft: '0', - })); + jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing); wrapper = mount( , ); @@ -259,10 +268,7 @@ describe('DraggableDimensionPublisher', () => { })); jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({ overflowY: 'scroll', - marginTop: '0', - marginRight: '0', - marginBottom: '0', - marginLeft: '0', + ...noSpacing, })); wrapper = mount( , @@ -398,13 +404,6 @@ describe('DraggableDimensionPublisher', () => { } } - const noMargin = { - marginTop: '0', - marginRight: '0', - marginBottom: '0', - marginLeft: '0', - }; - type ExecuteArgs = {| droppableRect: ClientRect, scrollParentRect: ClientRect, @@ -440,12 +439,12 @@ describe('DraggableDimensionPublisher', () => { jest.spyOn(window, 'getComputedStyle').mockImplementation((el) => { if (el === droppableNode) { - return noMargin; + return noSpacing; } if (el === scrollParentNode) { return { - ...noMargin, + ...noSpacing, overflow: 'auto', }; } @@ -893,12 +892,7 @@ describe('DraggableDimensionPublisher', () => { height: droppable.page.withoutMargin.height, width: droppable.page.withoutMargin.width, })); - jest.spyOn(window, 'getComputedStyle').mockImplementation(() => ({ - marginTop: '0', - marginRight: '0', - marginBottom: '0', - marginLeft: '0', - })); + jest.spyOn(window, 'getComputedStyle').mockImplementation(() => noSpacing); }); it('should publish whether the droppable is enabled when requested to publish', () => { From 5d58b251ff1a09a031fc66b29fb6b5d40c8dc683 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 15:05:30 +1000 Subject: [PATCH 114/117] fixing import --- src/state/get-client-rect.js | 1 - src/view/placeholder/index.js | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/get-client-rect.js b/src/state/get-client-rect.js index 65bdc423ae..ccaf222537 100644 --- a/src/state/get-client-rect.js +++ b/src/state/get-client-rect.js @@ -1,7 +1,6 @@ // @flow import type { Spacing, ClientRect } from '../types'; -// $ExpectError - flow cannot handle using an axis to create these export default ({ top, right, bottom, left }: Spacing): ClientRect => ({ top, right, diff --git a/src/view/placeholder/index.js b/src/view/placeholder/index.js index 7d723e8f35..eab54fa1b0 100644 --- a/src/view/placeholder/index.js +++ b/src/view/placeholder/index.js @@ -1 +1,2 @@ -export default from './placeholder'; +// @flow +export { default } from './placeholder'; From a580d3f02927ce77b3b061e51f2c187c12585995 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 15:09:38 +1000 Subject: [PATCH 115/117] fixing flow issues --- src/state/get-client-rect.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/state/get-client-rect.js b/src/state/get-client-rect.js index ccaf222537..2c579383a5 100644 --- a/src/state/get-client-rect.js +++ b/src/state/get-client-rect.js @@ -1,7 +1,11 @@ // @flow import type { Spacing, ClientRect } from '../types'; -export default ({ top, right, bottom, left }: Spacing): ClientRect => ({ +// Ideally we would just use the Spacing type here - but flow gets confused when +// dynamically creating a Spacing object from an axis +type ShouldBeSpacing = Object | Spacing + +export default ({ top, right, bottom, left }: ShouldBeSpacing): ClientRect => ({ top, right, bottom, From 4c7b08b8ff79fb7705bf12c5800eff520f084047 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 15:42:43 +1000 Subject: [PATCH 116/117] updating docs --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ee1ca22180..ed354a0039 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ class App extends Component { )}
))} + {provided.placeholder}
)} @@ -390,7 +391,7 @@ type DraggableLocation = {| ### Best practices for `hooks` -**Block updates during a drag** +#### Block updates during a drag It is **highly** recommended that while a user is dragging that you block any state updates that might impact the amount of `Draggable`s and `Droppable`s, or their dimensions. Please listen to `onDragStart` and block updates to the `Draggable`s and `Droppable`s until you receive at `onDragEnd`. @@ -404,12 +405,7 @@ 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 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. - -**`onDragStart` and `onDragEnd` pairing** - -We try very hard to ensure that each `onDragStart` event is paired with a single `onDragEnd` event. However, there maybe a rouge situation where this is not the case. If that occurs - it is a bug. Currently there is no mechanism to tell the library to cancel a current drag externally. - -**Style** +#### Add a cursor style and block selection During a drag it is recommended that you add two styles to the body: @@ -420,9 +416,24 @@ During a drag it is recommended that you add two styles to the body: `cursor: [your desired cursor];` is needed because we apply `pointer-events: none;` to the dragging item. This prevents you setting your own cursor style on the Draggable directly based on `snapshot.isDragging` (see `Draggable`). +#### Force focus after a transition between lists + +When an item is moved from one list to a different list it looses browser focus if it had it. This is because `React` creates a new node in this situation. It will not loose 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: + +- `onDragEnd`: move the item into the new list and record the id fo the item that has moved +- When rendering the reordered list pass down a prop which will tell the newly moved item to obtain focus +- In the `componentDidMount` lifecycle call back check if the item needs to gain focus based on its props (such as an `autoFocus` prop) +- If focus is required - call `.focus` on the node. You can obtain the node by using `ReactDOM.findDOMNode` or monkey patching the `provided.innerRef` callback. + +### Other `hooks` information + +**`onDragStart` and `onDragEnd` pairing** + +We try very hard to ensure that each `onDragStart` event is paired with a single `onDragEnd` event. However, there maybe a rouge situation where this is not the case. If that occurs - it is a bug. Currently there is no mechanism to tell the library to cancel a current drag externally. + **Dynamic hooks** -Your *hook* functions will only be captured *once at start up*. Please do not change the function after that. If there is a valid use case for this then dynamic hooks could be supported. However, at this time it is not. +Your *hook* functions will only be captured *once at start up*. Please do not change the function after that. This will be changed soon. ## `Droppable` @@ -437,7 +448,8 @@ import { Droppable } from 'react-beautiful-dnd'; ref={provided.innerRef} style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }} > - I am a droppable! +

I am a droppable!

+ {provided.placeholder} )} ; @@ -468,14 +480,23 @@ The function is provided with two arguments: ```js type DroppableProvided = {| innerRef: (?HTMLElement) => void, + placeholder: ?ReactElement, |} ``` -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. + +- `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 that you have provided the ref for. We need to increase the side of the `Droppable` itself. This is different from `Draggable` where the `placeholder` needs to be a *silbing* to the draggable node. ```js - {(provided, snapshot) =>
Good to go
} + {(provided, snapshot) => ( +
+ Good to go + + {provided.placeholder} +
+ )}
; ``` @@ -497,6 +518,8 @@ The `children` function is also provided with a small amount of state relating t style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }} > I am a droppable! + + {provided.placeholder} )} ; @@ -681,7 +704,7 @@ type NotDraggingStyle = {| |}; ``` -- `provided.placeholder (?ReactElement)` The `Draggable` element has `position: fixed` applied to it while it is dragging. The role of the `placeholder` is to sit in the place that the `Draggable` was during a drag. It is needed to stop the `Droppable` list from collapsing when you drag. It is advised to render it as a sibling to the `Draggable` node. When the library moves to `React` 16 the `placeholder` will be removed from api. +- `provided.placeholder (?ReactElement)` The `Draggable` element has `position: fixed` applied to it while it is dragging. The role of the `placeholder` is to sit in the place that the `Draggable` was during a drag. It is needed to stop the `Droppable` list from collapsing when you drag. It is advised to render it as a sibling to the `Draggable` node. This is unlike `Droppable` where the `placeholder` needs to be *within* the `Droppable` node. When the library moves to `React` 16 the `placeholder` will be removed from api. ```js @@ -856,6 +879,11 @@ type DraggableLocation = {| // Droppable type DroppableProvided = {| innerRef: (?HTMLElement) => void, + placeholder: ?ReactElement, +|} + +type DraggableStateSnapshot = {| + isDraggingOver: boolean, |} // Draggable @@ -865,6 +893,11 @@ type DraggableProvided = {| dragHandleProps: ?DragHandleProvided, placeholder: ?ReactElement, |} + +type DraggableStateSnapshot = {| + isDragging: boolean, +|} + type DraggableStyle = DraggingStyle | NotDraggingStyle type DraggingStyle = {| pointerEvents: 'none', From aa5ecbe924fbec87962f80dd012fc79862a38810 Mon Sep 17 00:00:00 2001 From: Alex Reardon Date: Thu, 14 Sep 2017 15:44:24 +1000 Subject: [PATCH 117/117] more updates to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed354a0039..311f943ccf 100644 --- a/README.md +++ b/README.md @@ -433,7 +433,7 @@ We try very hard to ensure that each `onDragStart` event is paired with a single **Dynamic hooks** -Your *hook* functions will only be captured *once at start up*. Please do not change the function after that. This will be changed soon. +Your *hook* functions will only be captured *once at start up*. Please do not change the function after that. This behaviour will be changed soon to allow dynamic hooks. ## `Droppable`