├── .browserlistrc ├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature-request.md │ └── tree-issue.md ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .size-snapshot.json ├── .storybook ├── .babelrc ├── babel-setup.md ├── compressed-logo-rbd.svg ├── config.js ├── custom-decorators │ └── global-styles.jsx ├── main.js └── preview-head.html ├── .stylelintrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── a11y-audit-parse.js ├── babel.config.js ├── browser-test-harness.js ├── csp-server ├── .eslintrc.js ├── app.jsx ├── client.js ├── main.js ├── server.js ├── start.sh └── webpack.config.js ├── cypress.json ├── cypress ├── .eslintrc.js ├── fixtures │ └── .gitkeep ├── integration │ ├── content-security-policy.spec.js │ ├── focus.spec.js │ ├── move-between-lists.spec.js │ ├── reorder-lists.spec.js │ ├── reorder-virtual.spec.js │ ├── reorder.spec.js │ └── util.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── docs ├── about │ ├── accessibility.md │ ├── animations.md │ ├── browser-support.md │ ├── design-principles.md │ ├── examples.md │ └── installation.md ├── api │ ├── drag-drop-context.md │ ├── draggable.md │ ├── droppable.md │ └── reset-server-context.md ├── guides │ ├── auto-scrolling.md │ ├── avoiding-image-flickering.md │ ├── browser-focus.md │ ├── changes-while-dragging.md │ ├── combining.md │ ├── common-setup-issues.md │ ├── content-security-policy.md │ ├── doctype.md │ ├── dragging-svgs.md │ ├── drop-animation.md │ ├── how-we-detect-scroll-containers.md │ ├── how-we-use-dom-events.md │ ├── identifiers.md │ ├── preset-styles.md │ ├── reparenting.md │ ├── responders.md │ ├── screen-reader.md │ ├── setup-problem-detection-and-error-recovery.md │ ├── types.md │ └── using-inner-ref.md ├── patterns │ ├── multi-drag.md │ ├── tables.md │ └── virtual-lists.md ├── sensors │ ├── keyboard.md │ ├── mouse.md │ ├── sensor-api.md │ └── touch.md └── support │ ├── community-and-addons.md │ ├── engineering-health.md │ ├── media.md │ └── upgrading.md ├── flow-typed ├── custom │ ├── cypress.js │ └── raf.js └── npm │ ├── @atlaskit │ ├── css-reset_vx.x.x.js │ └── theme_vx.x.x.js │ ├── @babel │ ├── core_vx.x.x.js │ ├── plugin-proposal-class-properties_vx.x.x.js │ ├── plugin-transform-modules-commonjs_vx.x.x.js │ ├── plugin-transform-object-assign_vx.x.x.js │ ├── plugin-transform-runtime_vx.x.x.js │ ├── preset-env_vx.x.x.js │ ├── preset-flow_vx.x.x.js │ ├── preset-react_vx.x.x.js │ └── runtime_vx.x.x.js │ ├── @emotion │ └── babel-preset-css-prop_vx.x.x.js │ ├── @storybook │ ├── react_v5.x.x.js │ └── theming_vx.x.x.js │ ├── @testing-library │ └── react_v9.x.x.js │ ├── babel-core_vx.x.x.js │ ├── babel-eslint_vx.x.x.js │ ├── babel-jest_vx.x.x.js │ ├── babel-loader_vx.x.x.js │ ├── babel-plugin-dev-expression_vx.x.x.js │ ├── cross-env_vx.x.x.js │ ├── cypress_vx.x.x.js │ ├── enzyme-adapter-react-16_vx.x.x.js │ ├── enzyme_v3.x.x.js │ ├── eslint-config-airbnb_vx.x.x.js │ ├── eslint-config-prettier_vx.x.x.js │ ├── eslint-plugin-cypress_vx.x.x.js │ ├── eslint-plugin-es5_vx.x.x.js │ ├── eslint-plugin-flowtype_vx.x.x.js │ ├── eslint-plugin-import_vx.x.x.js │ ├── eslint-plugin-jest_vx.x.x.js │ ├── eslint-plugin-jsx-a11y_vx.x.x.js │ ├── eslint-plugin-prettier_vx.x.x.js │ ├── eslint-plugin-react-hooks_vx.x.x.js │ ├── eslint-plugin-react_vx.x.x.js │ ├── eslint_vx.x.x.js │ ├── flow-bin_v0.x.x.js │ ├── fs-extra_vx.x.x.js │ ├── globby_vx.x.x.js │ ├── jest-axe_vx.x.x.js │ ├── jest-junit_vx.x.x.js │ ├── jest-watch-typeahead_vx.x.x.js │ ├── jest_vx.x.x.js │ ├── lighthouse_vx.x.x.js │ ├── markdown-it_vx.x.x.js │ ├── prettier_v1.x.x.js │ ├── react-redux_v7.x.x.js │ ├── react-test-renderer_v16.x.x.js │ ├── redux_v4.x.x.js │ ├── require-from-string_vx.x.x.js │ ├── rimraf_vx.x.x.js │ ├── rollup-plugin-babel_vx.x.x.js │ ├── rollup-plugin-commonjs_vx.x.x.js │ ├── rollup-plugin-json_vx.x.x.js │ ├── rollup-plugin-node-resolve_vx.x.x.js │ ├── rollup-plugin-replace_vx.x.x.js │ ├── rollup-plugin-size-snapshot_vx.x.x.js │ ├── rollup-plugin-strip_vx.x.x.js │ ├── rollup-plugin-terser_vx.x.x.js │ ├── rollup_vx.x.x.js │ ├── styled-components_vx.x.x.js │ ├── stylelint-config-prettier_vx.x.x.js │ ├── stylelint-config-recommended_vx.x.x.js │ ├── stylelint-config-standard_vx.x.x.js │ ├── stylelint-config-styled-components_vx.x.x.js │ ├── stylelint-processor-styled-components_vx.x.x.js │ ├── stylelint_vx.x.x.js │ ├── wait-port_vx.x.x.js │ └── webpack_v4.x.x.js ├── jest.config.js ├── package.json ├── renovate.json ├── rollup.config.js ├── server-ports.js ├── src ├── animation.js ├── debug │ ├── middleware │ │ ├── action-timing-average.js │ │ ├── action-timing.js │ │ ├── log.js │ │ └── user-timing.js │ └── timings.js ├── dev-warning.js ├── empty.js ├── index.js ├── invariant.js ├── native-with-fallback.js ├── screen-reader-message-preset.js ├── state │ ├── action-creators.js │ ├── auto-scroller │ │ ├── auto-scroller-types.js │ │ ├── can-scroll.js │ │ ├── fluid-scroller │ │ │ ├── config.js │ │ │ ├── did-start-in-scrollable-area.js │ │ │ ├── get-best-scrollable-droppable.js │ │ │ ├── get-droppable-scroll-change.js │ │ │ ├── get-percentage.js │ │ │ ├── get-scroll │ │ │ │ ├── adjust-for-size-limits.js │ │ │ │ ├── get-scroll-on-axis │ │ │ │ │ ├── dampen-value-by-time.js │ │ │ │ │ ├── get-distance-thresholds.js │ │ │ │ │ ├── get-value-from-distance.js │ │ │ │ │ ├── get-value.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── min-scroll.js │ │ │ │ └── index.js │ │ │ ├── get-window-scroll-change.js │ │ │ ├── index.js │ │ │ └── scroll.js │ │ ├── index.js │ │ └── jump-scroller.js │ ├── axis.js │ ├── calculate-drag-impact │ │ └── calculate-reorder-impact.js │ ├── can-start-drag.js │ ├── create-store.js │ ├── did-start-after-critical.js │ ├── dimension-marshal │ │ ├── dimension-marshal-types.js │ │ ├── dimension-marshal.js │ │ ├── get-initial-publish.js │ │ └── while-dragging-publisher.js │ ├── dimension-structures.js │ ├── droppable │ │ ├── get-droppable.js │ │ ├── is-home-of.js │ │ ├── scroll-droppable.js │ │ ├── should-use-placeholder.js │ │ ├── util │ │ │ ├── clip.js │ │ │ └── get-subject.js │ │ ├── what-is-dragged-over-from-result.js │ │ ├── what-is-dragged-over.js │ │ └── with-placeholder.js │ ├── get-center-from-impact │ │ ├── get-client-border-box-center │ │ │ ├── get-client-from-page-border-box-center.js │ │ │ └── index.js │ │ ├── get-page-border-box-center │ │ │ ├── index.js │ │ │ ├── when-combining.js │ │ │ └── when-reordering.js │ │ └── move-relative-to.js │ ├── get-combined-item-displacement.js │ ├── get-displaced-by.js │ ├── get-displacement-groups.js │ ├── get-drag-impact │ │ ├── get-combine-impact.js │ │ ├── get-reorder-impact.js │ │ └── index.js │ ├── get-draggables-inside-droppable.js │ ├── get-droppable-over.js │ ├── get-frame.js │ ├── get-home-location.js │ ├── get-impact-location.js │ ├── get-is-displaced.js │ ├── get-lift-effect.js │ ├── get-max-scroll.js │ ├── is-movement-allowed.js │ ├── is-within.js │ ├── middleware │ │ ├── auto-scroll.js │ │ ├── dimension-marshal-stopper.js │ │ ├── drop │ │ │ ├── drop-animation-finish-middleware.js │ │ │ ├── drop-animation-flush-on-scroll-middleware.js │ │ │ ├── drop-middleware.js │ │ │ ├── get-drop-duration.js │ │ │ ├── get-drop-impact.js │ │ │ ├── get-new-home-client-offset.js │ │ │ └── index.js │ │ ├── focus.js │ │ ├── lift.js │ │ ├── pending-drop.js │ │ ├── responders │ │ │ ├── async-marshal.js │ │ │ ├── expiring-announce.js │ │ │ ├── index.js │ │ │ ├── is-equal.js │ │ │ ├── publisher.js │ │ │ └── responders-middleware.js │ │ ├── scroll-listener.js │ │ ├── style.js │ │ └── util │ │ │ └── validate-dimensions.js │ ├── move-in-direction │ │ ├── index.js │ │ ├── move-cross-axis │ │ │ ├── get-best-cross-axis-droppable.js │ │ │ ├── get-closest-draggable.js │ │ │ ├── index.js │ │ │ ├── move-to-new-droppable.js │ │ │ └── without-starting-displacement.js │ │ ├── move-in-direction-types.js │ │ └── move-to-next-place │ │ │ ├── index.js │ │ │ ├── is-totally-visible-in-new-location.js │ │ │ ├── move-to-next-combine │ │ │ └── index.js │ │ │ └── move-to-next-index │ │ │ ├── from-combine.js │ │ │ ├── from-reorder.js │ │ │ └── index.js │ ├── no-impact.js │ ├── patch-dimension-map.js │ ├── patch-droppable-map.js │ ├── position.js │ ├── post-reducer │ │ └── when-moving │ │ │ ├── refresh-snap.js │ │ │ └── update.js │ ├── publish-while-dragging-in-virtual │ │ ├── adjust-additions-for-scroll-changes.js │ │ ├── index.js │ │ └── offset-draggable.js │ ├── recompute-placeholders.js │ ├── rect.js │ ├── reducer.js │ ├── registry │ │ ├── create-registry.js │ │ ├── registry-types.js │ │ └── use-registry.js │ ├── remove-draggable-from-list.js │ ├── scroll-viewport.js │ ├── spacing.js │ ├── store-types.js │ ├── update-displacement-visibility │ │ ├── recompute.js │ │ └── speculatively-increase.js │ ├── visibility │ │ ├── is-partially-visible-through-frame.js │ │ ├── is-position-in-frame.js │ │ ├── is-totally-visible-through-frame-on-axis.js │ │ ├── is-totally-visible-through-frame.js │ │ └── is-visible.js │ └── with-scroll-change │ │ ├── with-all-displacement.js │ │ ├── with-droppable-displacement.js │ │ ├── with-droppable-scroll.js │ │ └── with-viewport-displacement.js ├── types.js └── view │ ├── animate-in-out │ ├── animate-in-out.jsx │ └── index.js │ ├── check-is-valid-inner-ref.js │ ├── context │ ├── app-context.js │ ├── droppable-context.js │ └── store-context.js │ ├── data-attributes.js │ ├── drag-drop-context │ ├── app.jsx │ ├── check-doctype.js │ ├── check-react-version.js │ ├── drag-drop-context-types.js │ ├── drag-drop-context.jsx │ ├── error-boundary.jsx │ ├── index.js │ ├── use-startup-validation.js │ └── use-unique-context-id.js │ ├── draggable │ ├── connected-draggable.js │ ├── draggable-api.jsx │ ├── draggable-types.js │ ├── draggable.jsx │ ├── get-style.js │ ├── index.js │ └── use-validation.js │ ├── droppable │ ├── connected-droppable.js │ ├── droppable-types.js │ ├── droppable.jsx │ ├── index.js │ └── use-validation.js │ ├── event-bindings │ ├── bind-events.js │ └── event-types.js │ ├── get-body-element.js │ ├── get-border-box-center-position.js │ ├── get-document-element.js │ ├── get-elements │ ├── find-drag-handle.js │ └── find-draggable.js │ ├── is-strict-equal.js │ ├── is-type-of-element │ ├── is-element.js │ ├── is-html-element.js │ └── is-svg-element.js │ ├── key-codes.js │ ├── placeholder │ ├── index.js │ ├── placeholder-types.js │ └── placeholder.jsx │ ├── scroll-listener.js │ ├── throw-if-invalid-inner-ref.js │ ├── use-announcer │ ├── index.js │ └── use-announcer.js │ ├── use-dev-setup-warning.js │ ├── use-dev.js │ ├── use-draggable-publisher │ ├── get-dimension.js │ ├── index.js │ └── use-draggable-publisher.js │ ├── use-droppable-publisher │ ├── check-for-nested-scroll-container.js │ ├── get-closest-scrollable.js │ ├── get-dimension.js │ ├── get-env.js │ ├── get-listener-options.js │ ├── get-scroll.js │ ├── index.js │ ├── is-in-fixed-container.js │ └── use-droppable-publisher.js │ ├── use-focus-marshal │ ├── focus-marshal-types.js │ ├── index.js │ └── use-focus-marshal.js │ ├── use-hidden-text-element │ ├── index.js │ └── use-hidden-text-element.js │ ├── use-isomorphic-layout-effect.js │ ├── use-previous-ref.js │ ├── use-required-context.js │ ├── use-sensor-marshal │ ├── closest.js │ ├── find-closest-draggable-id-from-event.js │ ├── index.js │ ├── is-event-in-interactive-element.js │ ├── lock.js │ ├── sensors │ │ ├── use-keyboard-sensor.js │ │ ├── use-mouse-sensor.js │ │ ├── use-touch-sensor.js │ │ └── util │ │ │ ├── prevent-standard-key-events.js │ │ │ └── supported-page-visibility-event-name.js │ ├── use-sensor-marshal.js │ └── use-validate-sensor-hooks.js │ ├── use-style-marshal │ ├── get-styles.js │ ├── index.js │ ├── style-marshal-types.js │ └── use-style-marshal.js │ ├── use-unique-id.js │ ├── visually-hidden-style.js │ └── window │ ├── get-max-window-scroll.js │ ├── get-viewport.js │ ├── get-window-from-el.js │ ├── get-window-scroll.js │ └── scroll-window.js ├── stories ├── .eslintrc.js ├── 1-single-vertical-list.stories.js ├── 10-table.stories.js ├── 11-portal.stories.js ├── 12-dynamic.stories.js ├── 15-on-before-capture.stories.js ├── 2-single-horizontal.stories.js ├── 20-super-simple.stories.js ├── 21-change-on-drag-start.stories.js ├── 25-fixed-list.stories.js ├── 3-board.stories.stories.js ├── 30-custom-drop.stories.js ├── 35-function-component.stories.js ├── 4-complex-vertical-list.stories.js ├── 40-programmatic.stories.js ├── 45-virtual.stories.js ├── 5-multiple-vertical-lists.stories.js ├── 50-multiple-contexts.stories.js ├── 55-mixed-sizes.stories.js ├── 6-multiple-horizontal-lists.stories.js ├── 7-interactive-elements.stories.js ├── 8-accessibility.stories.js ├── 9-multi-drag.stories.js ├── 99-debug.stories.js ├── src │ ├── accessible │ │ ├── blur-context.js │ │ ├── data.js │ │ ├── task-app.jsx │ │ ├── task-list.jsx │ │ └── task.jsx │ ├── board │ │ ├── board.jsx │ │ └── column.jsx │ ├── constants.js │ ├── custom-drop │ │ ├── funny-drop.jsx │ │ └── no-drop.jsx │ ├── data.js │ ├── dynamic │ │ ├── lazy-loading.jsx │ │ └── with-controls.jsx │ ├── fixed-list │ │ └── fixed-sidebar.jsx │ ├── function-component │ │ └── quote-app.jsx │ ├── horizontal │ │ └── author-app.jsx │ ├── interactive-elements │ │ └── interactive-elements-app.jsx │ ├── mixed-sizes │ │ ├── mixed-size-items.jsx │ │ ├── mixed-size-lists-experiment.jsx │ │ └── mixed-size-lists.jsx │ ├── multi-drag │ │ ├── column.jsx │ │ ├── data.js │ │ ├── task-app.jsx │ │ ├── task.jsx │ │ ├── types.js │ │ └── utils.js │ ├── multiple-horizontal │ │ └── quote-app.jsx │ ├── multiple-vertical │ │ └── quote-app.jsx │ ├── on-before-capture │ │ └── adding-things.jsx │ ├── portal │ │ └── portal-app.jsx │ ├── primatives │ │ ├── author-item.jsx │ │ ├── author-list.jsx │ │ ├── quote-item.jsx │ │ ├── quote-list.jsx │ │ └── title.jsx │ ├── programmatic │ │ ├── multiple-contexts.jsx │ │ ├── runsheet.jsx │ │ └── with-controls.jsx │ ├── reorder.js │ ├── simple │ │ ├── simple-mixed-spacing.jsx │ │ ├── simple-scrollable.jsx │ │ └── simple.jsx │ ├── table │ │ ├── with-clone.jsx │ │ ├── with-dimension-locking.jsx │ │ ├── with-fixed-columns.jsx │ │ └── with-portal.jsx │ ├── types.js │ ├── vertical-grouped │ │ └── quote-app.jsx │ ├── vertical-nested │ │ ├── quote-app.jsx │ │ ├── quote-list.jsx │ │ └── types.js │ ├── vertical │ │ └── quote-app.jsx │ └── virtual │ │ ├── quote-count-chooser.jsx │ │ ├── react-virtualized │ │ ├── board.jsx │ │ ├── list.jsx │ │ └── window-list.jsx │ │ └── react-window │ │ ├── board.jsx │ │ └── list.jsx └── static │ └── media │ ├── bmo-min.png │ ├── bmo.png │ ├── finn-min.png │ ├── finn.png │ ├── jake-min.png │ ├── jake.png │ ├── princess-min.png │ └── princess.png ├── test-reports └── lighthouse │ └── .gitkeep ├── test ├── .eslintrc.js ├── env-setup.js ├── test-flow-types.js ├── test-setup.js ├── unit │ ├── dev-warning.spec.js │ ├── docs │ │ ├── content.spec.js │ │ └── no-broken-links.spec.js │ ├── health │ │ ├── drop-dev-warnings-for-prod.spec.js │ │ └── src-file-name-convention.spec.js │ ├── integration │ │ ├── accessibility │ │ │ └── axe-audit.spec.js │ │ ├── body-removal-before-unmount.spec.js │ │ ├── combine-on-start.spec.js │ │ ├── disable-on-start.spec.js │ │ ├── drag-drop-context │ │ │ ├── check-doctype.spec.js │ │ │ ├── check-react-version.spec.js │ │ │ ├── clashing-with-consumers-redux.spec.js │ │ │ ├── error-handling │ │ │ │ ├── error-in-react-tree.spec.js │ │ │ │ └── error-on-window.spec.js │ │ │ ├── on-before-capture │ │ │ │ ├── additions.spec.js │ │ │ │ └── removals.spec.js │ │ │ ├── reset-server-context.spec.js │ │ │ └── unmount.spec.js │ │ ├── drag-handle │ │ │ ├── keyboard-sensor │ │ │ │ ├── directional-movement.spec.js │ │ │ │ ├── no-click-blocking.spec.js │ │ │ │ ├── prevent-keyboard-scroll.spec.js │ │ │ │ ├── prevent-standard-keys-while-dragging.spec.js │ │ │ │ ├── starting-a-drag.spec.js │ │ │ │ └── stopping-a-drag.spec.js │ │ │ ├── mouse-sensor │ │ │ │ ├── cancel-while-pending.spec.js │ │ │ │ ├── click-blocking.spec.js │ │ │ │ ├── force-press.spec.js │ │ │ │ ├── prevent-standard-keys-while-dragging.spec.js │ │ │ │ ├── starting-a-dragging.spec.js │ │ │ │ └── stopping-a-drag.spec.js │ │ │ ├── sensor-marshal │ │ │ │ ├── click-blocking.spec.js │ │ │ │ ├── force-releasing-locks.spec.js │ │ │ │ ├── is-lock-claimed.spec.js │ │ │ │ ├── lock-context-isolation.spec.js │ │ │ │ ├── move-throttling.spec.js │ │ │ │ ├── no-double-lift.spec.js │ │ │ │ ├── obtaining-lock.spec.js │ │ │ │ └── outdated-locks.spec.js │ │ │ ├── shared-behaviours │ │ │ │ ├── abort-on-error.spec.js │ │ │ │ ├── cancel-while-dragging.spec.js │ │ │ │ ├── cannot-start-when-disabled.spec.js │ │ │ │ ├── cannot-start-when-something-else-has-lock.spec.js │ │ │ │ ├── cannot-start-when-unmounted.spec.js │ │ │ │ ├── cleanup.spec.js │ │ │ │ ├── contenteditable.spec.js │ │ │ │ ├── disable-default-sensors.spec.js │ │ │ │ ├── interactive-elements.spec.js │ │ │ │ ├── lock-released-mid-drag.spec.js │ │ │ │ ├── lock-released-pre-drag.spec.js │ │ │ │ ├── nested-handles.spec.js │ │ │ │ ├── no-dragging-svgs.spec.js │ │ │ │ ├── parent-rendering-should-not-kill-drag.spec.js │ │ │ │ └── validate-controls.spec.js │ │ │ └── touch-sensor │ │ │ │ ├── cancel-while-pending.spec.js │ │ │ │ ├── click-blocking.spec.js │ │ │ │ ├── context-menu-opt-out.spec.js │ │ │ │ ├── force-press.spec.js │ │ │ │ ├── starting-a-drag.spec.js │ │ │ │ ├── stopping-a-drag.spec.js │ │ │ │ └── unmounted-while-pending-timer-running.spec.js │ │ ├── draggable │ │ │ ├── combined-with.spec.js │ │ │ ├── dragging.spec.js │ │ │ ├── dropping.spec.js │ │ │ ├── moving-out-of-the-way.spec.js │ │ │ ├── portal.spec.js │ │ │ ├── resting.spec.js │ │ │ └── validation.spec.js │ │ ├── droppable │ │ │ ├── clone.spec.js │ │ │ └── placeholder.spec.js │ │ ├── reorder-render-sync.spec.js │ │ ├── responders-integration.spec.js │ │ ├── responders-timing.spec.js │ │ ├── server-side-rendering │ │ │ ├── __snapshots__ │ │ │ │ └── server-rendering.spec.js.snap │ │ │ ├── client-hydration.spec.js │ │ │ └── server-rendering.spec.js │ │ └── util │ │ │ ├── app.jsx │ │ │ ├── board.jsx │ │ │ ├── controls.js │ │ │ ├── expanded-mouse.js │ │ │ └── helpers.js │ ├── state │ │ ├── auto-scroll │ │ │ ├── can-scroll.spec.js │ │ │ ├── choosing-the-right-scroller.spec.js │ │ │ ├── fluid-scroller │ │ │ │ ├── big-draggables.spec.js │ │ │ │ ├── droppable-scrolling.spec.js │ │ │ │ ├── lifecycle.spec.js │ │ │ │ ├── time-dampening.spec.js │ │ │ │ ├── util │ │ │ │ │ ├── drag-to.js │ │ │ │ │ ├── for-each.js │ │ │ │ │ ├── get-args-mock.js │ │ │ │ │ ├── get-droppable.js │ │ │ │ │ └── viewport.js │ │ │ │ ├── window-before-droppable.spec.js │ │ │ │ └── window-scrolling.spec.js │ │ │ └── jump-scroller.spec.js │ │ ├── can-start-drag.spec.js │ │ ├── dimension-structures.spec.js │ │ ├── droppable │ │ │ ├── clip.spec.js │ │ │ ├── get-droppable.spec.js │ │ │ ├── get-subject.spec.js │ │ │ ├── is-home-of.spec.js │ │ │ ├── scroll-droppable.spec.js │ │ │ ├── what-is-dragged-over.spec.js │ │ │ └── with-placeholder.spec.js │ │ ├── get-center-from-impact │ │ │ ├── get-client-border-box-center.spec.js │ │ │ ├── get-client-from-page-border-box-center.spec.js │ │ │ ├── get-page-border-box-center │ │ │ │ ├── combine │ │ │ │ │ ├── when-combining.spec.js │ │ │ │ │ └── with-droppable-scroll.spec.js │ │ │ │ ├── in-home-location.spec.js │ │ │ │ ├── over-nothing.spec.js │ │ │ │ └── reorder │ │ │ │ │ ├── in-empty-list.spec.js │ │ │ │ │ ├── nothing-displaced.spec.js │ │ │ │ │ ├── there-is-displacement.spec.js │ │ │ │ │ └── with-droppable-scroll.spec.js │ │ │ └── move-relative-to.spec.js │ │ ├── get-displacement-groups │ │ │ ├── get-displacement-groups.spec.js │ │ │ └── use-initial-position-not-displaced.spec.js │ │ ├── get-drag-impact │ │ │ ├── combine │ │ │ │ ├── is-combine-disabled.spec.js │ │ │ │ ├── should-not-combine-with-home-draggable.spec.js │ │ │ │ ├── started-after-critical.spec.js │ │ │ │ ├── started-before-critical.spec.js │ │ │ │ └── with-droppable-scroll.spec.js │ │ │ ├── is-disabled.spec.js │ │ │ ├── over-nothing.spec.js │ │ │ ├── reorder │ │ │ │ ├── over-foreign-list │ │ │ │ │ ├── did-not-start-displaced.spec.js │ │ │ │ │ ├── move-backward-from-last-item.spec.js │ │ │ │ │ └── move-past-last-item.spec.js │ │ │ │ └── over-home-list │ │ │ │ │ ├── displacement-visibility.spec.js │ │ │ │ │ ├── move-past-last-item.spec.js │ │ │ │ │ ├── started-after-critical.spec.js │ │ │ │ │ ├── started-before-critical.spec.js │ │ │ │ │ └── with-droppable-scroll.spec.js │ │ │ └── util │ │ │ │ ├── get-combine-threshold.js │ │ │ │ └── get-offset-for-edge.js │ │ ├── get-draggables-inside-droppable.spec.js │ │ ├── get-droppable-over │ │ │ ├── center-is-over.spec.js │ │ │ ├── is-disabled.spec.js │ │ │ ├── is-not-visible.spec.js │ │ │ ├── is-over-nothing.spec.js │ │ │ ├── item-edge-is-over-list-center.spec.js │ │ │ ├── item-is-totally-over.spec.js │ │ │ └── preferencing.spec.js │ │ ├── get-lift-effect │ │ │ └── get-lift-effect.spec.js │ │ ├── is-within.spec.js │ │ ├── middleware │ │ │ ├── auto-scroll.spec.js │ │ │ ├── dimension-marshal-stopper.spec.js │ │ │ ├── drop │ │ │ │ ├── conditionally-animate-drop.spec.js │ │ │ │ ├── drop-animation-finish-middleware.spec.js │ │ │ │ ├── drop-animation-flush-on-scroll-middleware.spec.js │ │ │ │ ├── drop-impact.spec.js │ │ │ │ ├── drop-position.spec.js │ │ │ │ ├── get-drop-duration.spec.js │ │ │ │ ├── result-impact-mismatch.spec.js │ │ │ │ └── timing.spec.js │ │ │ ├── lift.spec.js │ │ │ ├── pending-drop.spec.js │ │ │ ├── responders │ │ │ │ ├── abort.spec.js │ │ │ │ ├── announcements.spec.js │ │ │ │ ├── drop.spec.js │ │ │ │ ├── flushing.spec.js │ │ │ │ ├── repeated-use.spec.js │ │ │ │ ├── start.spec.js │ │ │ │ ├── update.spec.js │ │ │ │ └── util │ │ │ │ │ ├── get-announce-stub.js │ │ │ │ │ ├── get-completed-with-result.js │ │ │ │ │ └── get-responders-stub.js │ │ │ ├── style.spec.js │ │ │ ├── util │ │ │ │ ├── create-store.js │ │ │ │ └── pass-through-middleware.js │ │ │ └── validate-indexes.spec.js │ │ ├── move-in-direction │ │ │ ├── move-cross-axis │ │ │ │ ├── get-best-cross-axis-droppable.spec.js │ │ │ │ ├── get-closest-draggable │ │ │ │ │ ├── with-starting-displacement.spec.js │ │ │ │ │ └── without-starting-displacement.spec.js │ │ │ │ ├── move-to-new-droppable │ │ │ │ │ ├── to-foreign-list.spec.js │ │ │ │ │ └── to-home-list.spec.js │ │ │ │ └── no-visible-targets-in-list.spec.js │ │ │ ├── move-in-direction.spec.js │ │ │ └── move-to-next-place │ │ │ │ ├── move-to-next-combine │ │ │ │ ├── in-foreign-list.legacy.spec.js │ │ │ │ └── in-home-list.legacy.spec.js │ │ │ │ ├── move-to-next-index │ │ │ │ ├── from-combine │ │ │ │ │ ├── did-not-start-after-critical.spec.js │ │ │ │ │ └── started-after-critical.spec.js │ │ │ │ └── from-reorder │ │ │ │ │ ├── in-foreign-list.spec.js │ │ │ │ │ └── in-home-list.spec.js │ │ │ │ └── moving-to-invisible-place │ │ │ │ ├── not-visible-in-droppable.spec.js │ │ │ │ └── not-visible-in-viewport.spec.js │ │ ├── position.spec.js │ │ ├── post-reducer │ │ │ └── .gitkeep │ │ ├── publish-while-dragging │ │ │ ├── adjust-additions-for-scroll-change.spec.js │ │ │ ├── displacement-animation.spec.js │ │ │ ├── droppable-scroll-change.spec.js │ │ │ ├── nothing-changed.spec.js │ │ │ ├── phase-change.spec.js │ │ │ ├── recompute-after-critical.spec.js │ │ │ └── util.js │ │ ├── recompute-placeholders.spec.js │ │ ├── registry │ │ │ ├── cleanup.spec.js │ │ │ ├── draggable-registration.spec.js │ │ │ ├── droppable-registration.spec.js │ │ │ ├── event-listeners.spec.js │ │ │ ├── queries.spec.js │ │ │ └── use-registry.spec.js │ │ ├── scroll-viewport.spec.js │ │ ├── spacing.spec.js │ │ ├── update-displacement-visibility │ │ │ ├── recompute.spec.js │ │ │ └── speculative-displacement.spec.js │ │ └── visibility │ │ │ ├── is-partially-visible-through-frame.spec.js │ │ │ ├── is-partially-visible.spec.js │ │ │ ├── is-position-in-frame.spec.js │ │ │ ├── is-totally-visible-on-axis.spec.js │ │ │ ├── is-totally-visible-through-frame.spec.js │ │ │ └── is-totally-visible.spec.js │ └── view │ │ ├── animate-in-out │ │ ├── animate-in-out.spec.js │ │ └── child-rendering.spec.js │ │ ├── announcer.spec.js │ │ ├── connected-draggable │ │ ├── child-render-behaviour.spec.js │ │ ├── combine-target-for.spec.js │ │ ├── combine-with.spec.js │ │ ├── dragging.spec.js │ │ ├── dropping-something-else.spec.js │ │ ├── dropping-with-result-mismatch.spec.js │ │ ├── dropping.spec.js │ │ ├── nothing-is-dragging.spec.js │ │ ├── selector-isolation.spec.js │ │ ├── something-else-dragging-in-virtual.spec.js │ │ ├── something-else-is-dragging.spec.js │ │ └── util │ │ │ ├── get-dragging-map-props.js │ │ │ ├── get-own-props.js │ │ │ ├── get-secondary-map-props.js │ │ │ └── get-snapshot.js │ │ ├── connected-droppable │ │ ├── child-render-behaviour.spec.js │ │ ├── disabled.spec.js │ │ ├── dragging.spec.js │ │ ├── dropping.spec.js │ │ ├── post-drop.spec.js │ │ ├── selector-isolation.spec.js │ │ └── util │ │ │ ├── get-own-props.js │ │ │ ├── resting-props.js │ │ │ └── with-combine-impact.js │ │ ├── dimension-marshal │ │ ├── droppable-passthrough.spec.js │ │ ├── initial-publish.spec.js │ │ ├── publish-while-dragging.spec.js │ │ └── util.js │ │ ├── drag-drop-context │ │ └── content-security-protection-nonce.spec.js │ │ ├── droppable │ │ ├── home-list-placeholder-cleanup.spec.js │ │ ├── inner-ref-validation.spec.js │ │ ├── own-props-validation.spec.js │ │ ├── pass-through-snapshot.spec.js │ │ ├── placeholder-setup-warning.spec.js │ │ ├── placeholder.spec.js │ │ ├── update-max-window-scroll.spec.js │ │ └── util │ │ │ ├── get-props.js │ │ │ ├── get-stubber.js │ │ │ └── mount.js │ │ ├── is-type-of-element │ │ ├── is-element.spec.js │ │ ├── is-html-element.spec.js │ │ ├── is-svg-element.spec.js │ │ └── util │ │ │ └── get-svg.js │ │ ├── placeholder │ │ ├── animated-mount.spec.js │ │ ├── on-close.spec.js │ │ ├── on-transition-end.spec.js │ │ └── util │ │ │ ├── data.js │ │ │ ├── expect.js │ │ │ ├── get-placeholder-style.js │ │ │ └── placeholder-with-class.js │ │ ├── style-marshal │ │ ├── get-styles.spec.js │ │ └── style-marshal.spec.js │ │ ├── use-draggable-publisher.spec.js │ │ └── use-droppable-publisher │ │ ├── forced-scroll.spec.js │ │ ├── is-combined-enabled-change.spec.js │ │ ├── is-element-scrollable.spec.js │ │ ├── is-enabled-change.spec.js │ │ ├── publishing.spec.js │ │ ├── recollection.spec.js │ │ ├── registration.spec.js │ │ ├── scroll-watching.spec.js │ │ └── util │ │ └── shared.js └── util │ ├── after-point.js │ ├── before-point.js │ ├── cause-runtime-error.js │ ├── clone-impact.js │ ├── console.js │ ├── create-ref.js │ ├── dimension-marshal.js │ ├── dimension.js │ ├── dragging-state.js │ ├── force-update.js │ ├── get-simple-state-preset.js │ ├── impact.js │ ├── no-after-critical.js │ ├── pass-through-props.jsx │ ├── preset-action-args.js │ ├── registry.js │ ├── reorder.js │ ├── set-window-scroll-size.js │ ├── set-window-scroll.js │ ├── spacing.js │ ├── try-clean-prototype-stubs.js │ ├── user-input-util.js │ └── viewport.js └── yarn.lock /.browserlistrc: -------------------------------------------------------------------------------- 1 | ie >= 11 2 | last 1 Edge version 3 | last 1 Firefox version 4 | last 1 Chrome version 5 | last 1 Safari version 6 | last 1 iOS version 7 | last 1 Android version 8 | last 1 ChromeAndroid version 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | dist/ 3 | flow-typed/ 4 | site/ 5 | coverage/ 6 | babel.config.js 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [untyped] 2 | # Issue with atlaskit/theme typing 3 | .*/node_modules/@atlaskit/theme 4 | 5 | [ignore] 6 | # Creating lots of invalid files 7 | .*/node_modules/jsonlint-mod/.* 8 | 9 | [libs] 10 | ./flow-typed/custom/ 11 | 12 | [options] 13 | 14 | # Provides a way to suppress flow errors in the following line. 15 | # Example: // $FlowFixMe: This following line is borked because of reasons. 16 | # Example: // $ExpectError: This following line is correctly ignored by flow. 17 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 18 | suppress_comment= \\(.\\|\n\\)*\\$ExpectError 19 | include_warnings=true 20 | # Fixing issue with CircleCI where flow would hang 21 | # I suspect this is caused by incorrect advertisement of virtual cores 22 | server.max_workers=1 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡Feature request 3 | about: Ideas and suggestions 4 | labels: "idea \U0001F914, untriaged" 5 | --- 6 | 7 | 18 | 19 | ### Description 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/tree-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🌲@atlaskit/tree 3 | about: Bugs and feature requests for @atlaskit/tree 4 | labels: 'wontfix ☠️' 5 | --- 6 | 7 | Please head to the [`@atlaskit/tree` issue tracker](https://ecosystem.atlassian.net/servicedesk/customer/portal/24/create/236). 8 | 9 | We do not track `@atlaskit/tree` issues in the `react-beautiful-dnd` project 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editors 2 | .idea 3 | .vscode 4 | 5 | # library 6 | node_modules/ 7 | 8 | # MacOS 9 | .DS_Store 10 | 11 | # generated files 12 | dist/ 13 | 14 | # generated site 15 | site/ 16 | 17 | # coverage reports 18 | coverage/ 19 | 20 | # test reports 21 | test-reports/ 22 | 23 | # test outputs 24 | cypress/videos/ 25 | cypress/screenshots/ 26 | 27 | # storybook 28 | .storybook.out 29 | .cache/ 30 | 31 | # logs 32 | yarn-error.log 33 | npm-debug.log 34 | npm-debug.log.* 35 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.24.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist/react-beautiful-dnd.js": { 3 | "bundled": 364998, 4 | "minified": 133574, 5 | "gzipped": 39437 6 | }, 7 | "dist/react-beautiful-dnd.min.js": { 8 | "bundled": 306810, 9 | "minified": 108365, 10 | "gzipped": 31340 11 | }, 12 | "dist/react-beautiful-dnd.esm.js": { 13 | "bundled": 240910, 14 | "minified": 125371, 15 | "gzipped": 32650, 16 | "treeshaked": { 17 | "rollup": { 18 | "code": 21121, 19 | "import_statements": 503 20 | }, 21 | "webpack": { 22 | "code": 24005 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/react", 4 | "@babel/flow", 5 | ["@babel/env", { "modules": false, "loose": true }], 6 | "@emotion/babel-preset-css-prop" 7 | ], 8 | "plugins": [ 9 | "emotion", 10 | ["@babel/proposal-class-properties", { "loose": true }], 11 | ["@babel/proposal-object-rest-spread", { "loose": true }] 12 | ], 13 | "comments": false 14 | } 15 | -------------------------------------------------------------------------------- /.storybook/babel-setup.md: -------------------------------------------------------------------------------- 1 | # Babel 2 | 3 | storybook looks for a root `.babelrc` in the project for its babel config. However, we are using `.babelrc.js` which is not supported. Rather than putting effort into this we are just having a custom `.babelrc` in this folder which is the same as `.babelrc.js`. This is lame, but we are looking to move away from storybook in the short term anyway. 4 | 5 | - [Storybook babel docs](https://storybook.js.org/configurations/custom-babel-config/) 6 | - [Storybook issue for supporting `babelrc.js`](https://github.com/storybooks/storybook/issues/2582) 7 | -------------------------------------------------------------------------------- /.storybook/custom-decorators/global-styles.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from '@emotion/styled'; 4 | import { colors } from '@atlaskit/theme'; 5 | import { grid } from '../../stories/src/constants'; 6 | 7 | // $ExpectError - not sure why 8 | const GlobalStyles = styled.div` 9 | min-height: 100vh; 10 | color: ${colors.N900}; 11 | `; 12 | 13 | const GlobalStylesDecorator = (storyFn: Function) => ( 14 | {storyFn()} 15 | ); 16 | 17 | export default GlobalStylesDecorator; 18 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | addons: ['storybook-addon-performance/register','@storybook/addon-storysource'], 3 | }; 4 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "processors": [ 3 | [ 4 | "stylelint-processor-styled-components", 5 | { 6 | "moduleName": "@emotion/styled" 7 | } 8 | ] 9 | ], 10 | "extends": [ 11 | "stylelint-config-standard", 12 | "stylelint-config-styled-components", 13 | "stylelint-config-prettier" 14 | ], 15 | "rules": { 16 | "declaration-empty-line-before": null, 17 | "comment-empty-line-before": null, 18 | "block-no-empty": null, 19 | "value-keyword-case": null 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This project adheres to [Semantic Versioning 2.0](http://semver.org/). 4 | All release notes and upgrade notes can be found on our [Github Releases](https://github.com/atlassian/react-beautiful-dnd/releases) page. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Atlassian Pty Ltd 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /a11y-audit-parse.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | /* eslint-disable import/no-unresolved */ 3 | /* eslint-disable global-require */ 4 | /* eslint-disable no-console */ 5 | try { 6 | // I disabled flow for this file because of this line. 7 | // Sometimes the file exists, sometimes it doesn't. 8 | // It depends on if you have run the accessibility test or not. 9 | // Given this conditional I thought it best to simply disable flow for the file 10 | const a11yReport = require('./test-reports/lighthouse/a11y.report.json'); 11 | const a11yScore = a11yReport.categories.accessibility.score; 12 | const a11yScoreFormatted = `${a11yScore ? a11yScore * 100 : 0}%`; 13 | 14 | console.log('*************************'); 15 | console.log('Lighthouse accessibility score: ', a11yScoreFormatted); 16 | console.log('*************************'); 17 | 18 | if (a11yScore === 1) { 19 | // success! 20 | process.exit(0); 21 | } else { 22 | // fail build 23 | console.log( 24 | '\nNOTE: Lighthouse accessibility audit score must be 100% to pass this build step.\n\n', 25 | ); 26 | process.exit(1); 27 | } 28 | } catch (e) { 29 | console.error(e); 30 | process.exit(1); 31 | } 32 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/react', '@babel/flow', ['@babel/env', { loose: true }]], 3 | plugins: [ 4 | '@babel/transform-object-assign', 5 | ['@babel/proposal-class-properties', { loose: true }], 6 | // used for stripping out the `invariant` messages in production builds 7 | 'dev-expression', 8 | ], 9 | comments: false, 10 | }; 11 | -------------------------------------------------------------------------------- /csp-server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // allowing console.warn / console.error 4 | // this is because we often mock console.warn and console.error and adding this rul 5 | // avoids needing to constantly be opting out of the rule 6 | 'no-console': ['error', { allow: ['warn', 'error'] }], 7 | 8 | // allowing useMemo and useCallback in tests 9 | 'no-restricted-imports': 'off', 10 | 11 | // Allowing Array.from 12 | 'no-restricted-syntax': 'off', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /csp-server/client.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-env browser */ 3 | 4 | import React from 'react'; 5 | import { hydrate } from 'react-dom'; 6 | import Sample from './app'; 7 | 8 | const root: Element | null = document.getElementById('root'); 9 | 10 | let nonce = null; 11 | 12 | const cspEl = document.getElementById('csp-nonce'); 13 | if (cspEl) { 14 | nonce = cspEl.getAttribute('content'); 15 | } 16 | 17 | if (root) { 18 | hydrate(, root); 19 | } 20 | -------------------------------------------------------------------------------- /csp-server/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable id-length */ 2 | // @flow 3 | 4 | const webpack = require('webpack'); 5 | const requireFromString = require('require-from-string'); 6 | const MemoryFS = require('memory-fs'); 7 | const path = require('path'); 8 | const config = require('./webpack.config'); 9 | const ports = require('../server-ports'); 10 | 11 | const fs = new MemoryFS(); 12 | const compiler = webpack(config); 13 | 14 | // $ExpectError 15 | compiler.outputFileSystem = fs; 16 | // $ExpectError 17 | const outputPath = compiler.compilers.find((cfg) => cfg.name === 'client') 18 | .outputPath; 19 | 20 | compiler.run(() => { 21 | const content = fs.readFileSync(path.resolve(outputPath, 'server.js')); 22 | const server = requireFromString(content.toString()).default; 23 | server(process.argv[2] || ports.cspServer, outputPath, fs); 24 | }); 25 | -------------------------------------------------------------------------------- /csp-server/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./main'); 4 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:9002" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['cypress'], 3 | env: { 4 | 'cypress/globals': true, 5 | }, 6 | extends: ['plugin:cypress/recommended'], 7 | rules: { 8 | // Allowing Array.from 9 | 'no-restricted-syntax': 'off', 10 | 11 | // not using jest expects 12 | 'jest/expect-expect': 'off', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /cypress/fixtures/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/cypress/fixtures/.gitkeep -------------------------------------------------------------------------------- /cypress/integration/reorder-lists.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as keyCodes from '../../src/view/key-codes'; 3 | import { timings } from '../../src/animation'; 4 | import { getHandleSelector } from './util'; 5 | 6 | describe('reorder lists', () => { 7 | beforeEach(() => { 8 | cy.visit('/iframe.html?id=board--simple'); 9 | }); 10 | 11 | it('should reorder lists', () => { 12 | // order: Jake, BMO 13 | cy.get('h4').eq(0).as('first').should('contain', 'Jake'); 14 | 15 | cy.get('h4').eq(1).should('contain', 'BMO'); 16 | 17 | // reorder operation 18 | cy.get('@first') 19 | .closest(getHandleSelector()) 20 | .focus() 21 | .trigger('keydown', { keyCode: keyCodes.space }) 22 | .trigger('keydown', { keyCode: keyCodes.arrowRight, force: true }) 23 | // finishing before the movement time is fine - but this looks nice 24 | .wait(timings.outOfTheWay * 1000) 25 | .trigger('keydown', { keyCode: keyCodes.space, force: true }); 26 | 27 | // order now 2, 1 28 | // note: not using get aliases as they where returning incorrect results 29 | cy.get('h4').eq(0).should('contain', 'BMO'); 30 | 31 | // index of the drag handle has changed 32 | cy.get('h4').eq(1).should('contain', 'Jake'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /cypress/integration/util.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as dataAttr from '../../src/view/data-attributes'; 3 | 4 | export function getDroppableSelector(droppableId?: string) { 5 | if (droppableId) { 6 | return `[${dataAttr.droppable.id}="${droppableId}"]`; 7 | } 8 | return `[${dataAttr.droppable.id}]`; 9 | } 10 | 11 | export function getHandleSelector(draggableId?: string) { 12 | if (draggableId) { 13 | return `[${dataAttr.dragHandle.draggableId}="${draggableId}"]`; 14 | } 15 | return `[${dataAttr.dragHandle.draggableId}]`; 16 | } 17 | 18 | export function getDraggableSelector(draggableId?: string) { 19 | if (draggableId) { 20 | return `[${dataAttr.draggable.id}="${draggableId}"]`; 21 | } 22 | return `[${dataAttr.draggable.id}]`; 23 | } 24 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable flowtype/require-valid-file-annotation */ 3 | // *********************************************************** 4 | // This example plugins/index.js can be used to load plugins 5 | // 6 | // You can change the location of this file or turn off loading 7 | // the plugins file with the 'pluginsFile' configuration option. 8 | // 9 | // You can read more here: 10 | // https://on.cypress.io/plugins-guide 11 | // *********************************************************** 12 | 13 | // This function is called when a project is opened or re-opened (e.g. due to 14 | // the project's config changing) 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | }; 20 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // *********************************************** 3 | // This example commands.js shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add("login", (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This is will overwrite an existing command -- 26 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })// @flow 27 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // *********************************************************** 3 | // This example support/index.js is processed and 4 | // loaded automatically before your test files. 5 | // 6 | // This is a great place to put global configuration and 7 | // behavior that modifies Cypress. 8 | // 9 | // You can change the location of this file or turn off 10 | // automatically serving support files with the 11 | // 'supportFile' configuration option. 12 | // 13 | // You can read more here: 14 | // https://on.cypress.io/configuration 15 | // *********************************************************** 16 | 17 | // Import commands.js using ES2015 syntax: 18 | import './commands'; 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /docs/about/accessibility.md: -------------------------------------------------------------------------------- 1 | # Accessibility ♿️ 2 | 3 | Traditionally drag and drop interactions have been exclusively a mouse or touch interaction. This library has invested a huge amount of effort to ensure that everybody has access to drag and drop interactions 4 | 5 | ## What we do to include everyone 6 | 7 | - [Full keyboard support](/docs/sensors/keyboard.md) (reordering, combining, moving between lists) 8 | - [Keyboard multi drag support](/docs/patterns/multi-drag.md) 9 | - Keyboard [auto scrolling](/docs/guides/auto-scrolling.md) 10 | - Fantastic [screen reader support](/docs/guides/screen-reader.md) - _We ship with english messaging out of the box 📦_ 11 | - Smart management of [browser focus](/docs/guides/browser-focus.md) 12 | - A [Google lighthouse](https://developers.google.com/web/tools/lighthouse) automated build to ensure perfect accessibility scores (at least according to [Google](https://developers.google.com/web/tools/lighthouse/v3/scoring#a11y)) 13 | 14 | ![screen-reader-text](https://user-images.githubusercontent.com/2182637/36571009-d326d82a-1888-11e8-9a1d-e44f8b969c2f.gif) 15 | 16 | > Example screen reader announcement 17 | 18 | [← Back to documentation](/README.md#documentation-) 19 | -------------------------------------------------------------------------------- /docs/about/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 🎉 2 | 3 | See how beautiful it is for yourself! 4 | 5 | ## Viewing on a desktop 6 | 7 | [All the examples!](https://react-beautiful-dnd.netlify.app) 8 | 9 | ## Viewing on a mobile or tablet 10 | 11 | - [Simple list](https://react-beautiful-dnd.netlify.app/iframe.html?id=single-vertical-list--basic) 12 | - [Board](https://react-beautiful-dnd.netlify.app/iframe.html?id=board--simple) - best viewed in landscape 13 | 14 | > We provide different links for touch devices as [storybook](https://github.com/storybooks/storybook) runs examples in an iframe which can result in a strange auto scroll experience 15 | 16 | ## Basic samples 17 | 18 | We have created some basic examples on `codesandbox` for you to play with directly: 19 | 20 | - [Simple vertical list](https://codesandbox.io/s/k260nyxq9v) 21 | - [Simple horizontal list](https://codesandbox.io/s/mmrp44okvj) 22 | - [Using with function components](https://codesandbox.io/s/zqwz5n5p9x) 23 | - [Simple DnD between two lists](https://codesandbox.io/s/ql08j35j3q) - _Community made_ 24 | - [Simple DnD between a dynamic number of lists (with function components) and ability to delete items](https://codesandbox.io/s/-w5szl) - _Community made_ 25 | 26 | [← Back to documentation](/README.md#documentation-) 27 | -------------------------------------------------------------------------------- /docs/api/reset-server-context.md: -------------------------------------------------------------------------------- 1 | # `resetServerContext` 2 | 3 | The `resetServerContext` function should be used when server side rendering (SSR). It ensures context state does not persist across multiple renders on the server which would result in client/server markup mismatches after multiple requests are rendered on the server. 4 | 5 | Use it before calling the server side render method: 6 | 7 | ```js 8 | import { resetServerContext } from 'react-beautiful-dnd'; 9 | import { renderToString } from 'react-dom/server'; 10 | 11 | // ... 12 | 13 | resetServerContext(); 14 | renderToString(...); 15 | ``` 16 | 17 | [← Back to documentation](/README.md#documentation-) 18 | -------------------------------------------------------------------------------- /docs/guides/doctype.md: -------------------------------------------------------------------------------- 1 | # Use the html5 `doctype` 2 | 3 | Be sure that you have specified the html5 `doctype` ([Document Type Definition - DTD](https://developer.mozilla.org/en-US/docs/Glossary/Doctype)) for your `html` page: 4 | 5 | ```html 6 | 7 | ``` 8 | 9 | A `doctype` impacts browser layout and measurement apis. Not specifying a `doctype` is a world of pain 🔥. Browsers will use some other `doctype` such as ["Quirks mode"](https://en.wikipedia.org/wiki/Quirks_mode) which can drastically change layout and measurement ([more information](https://www.w3.org/QA/Tips/Doctype)). The html5 `doctype` is our only supported `doctype`. 10 | 11 | For non `production` builds we will log a warning to the `console` if a html5 `doctype` is not found. You can [disable the warning](#disable-warnings) if you like. 12 | 13 | [← Back to documentation](/README.md#documentation-) 14 | -------------------------------------------------------------------------------- /docs/support/community-and-addons.md: -------------------------------------------------------------------------------- 1 | # Community and addons 2 | 3 | ## Community ❤️👋 4 | 5 | - [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. 6 | - [react-beautiful-dnd-test-utils](https://github.com/colinrcummings/react-beautiful-dnd-test-utils) - 🧤 Test utils for `react-beautiful-dnd` built with `react-testing-library`. 7 | - Simple Trello - A simple cloning version of Trello, using React ecosystem. 8 | - [Demo](https://simple-trello.netlify.app/) 9 | - [Source](https://github.com/ng-hai/simple-trello) 10 | - 🎮 A drag'n'drop Checkers game 11 | - [Demo](https://checkers-game.netlify.app/) 12 | - [Source](https://github.com/emanuellarini/checkers) 13 | - [react-kanban](https://github.com/lourenci/react-kanban) - Another Kanban/Trello board lib for React, with customizations to use in projects. 14 | 15 | ## Addons 16 | 17 | - [natural-drag-animation-rbdnd](https://github.com/rokborf/natural-drag-animation-rbdnd) adds natural dragging animation 18 | 19 | [← Back to documentation](/README.md#documentation-) 20 | -------------------------------------------------------------------------------- /docs/support/upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | We have created upgrade instructions in our [Github release notes](https://github.com/atlassian/react-beautiful-dnd/releases) to help you upgrade to the latest version! 4 | 5 | - [Upgrading from `9.x` to `10.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v10.0.0) 6 | - [Upgrading from `8.x` to `9.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v9.0.0) 7 | - [Upgrading from `7.x` to `8.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v8.0.0) 8 | - [Upgrading from `6.x` to `7.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v7.0.0) 9 | - [Upgrading from `5.x` to `6.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v6.0.0) 10 | - [Upgrading from `4.x` to `5.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v5.0.0) 11 | - [Upgrading from `3.x` to `4.x`](https://github.com/atlassian/react-beautiful-dnd/releases/tag/v4.0.0) 12 | 13 | [← Back to documentation](/README.md#documentation-) 14 | -------------------------------------------------------------------------------- /flow-typed/custom/cypress.js: -------------------------------------------------------------------------------- 1 | // There is currently no flowtype definition for cypress 2 | // Currently just opting out of flow for the cy global 3 | // https://github.com/cypress-io/cypress/issues/2732 4 | // https://github.com/flow-typed/flow-typed/pull/3028 5 | declare var cy: any; 6 | -------------------------------------------------------------------------------- /flow-typed/custom/raf.js: -------------------------------------------------------------------------------- 1 | declare function raf(callback: (timestamp: number) => void): AnimationFrameID; 2 | 3 | // TODO: would like to use `import type {Stub} from 'raf-stub' 4 | // This is not supported right now: https://github.com/flow-typed/flow-typed/issues/2023 5 | declare var requestAnimationFrame: { 6 | add: (cb: Function) => number, 7 | remove: (id: number) => void, 8 | flush: (duration?: number) => void, 9 | reset: () => void, 10 | step: (steps?: number, duration?: number) => void, 11 | } & typeof raf; 12 | -------------------------------------------------------------------------------- /flow-typed/npm/@babel/plugin-transform-object-assign_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 00fa6c27d03ce929444546477c2d34ce 2 | // flow-typed version: <>/@babel/plugin-transform-object-assign_v^7.8.3/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * '@babel/plugin-transform-object-assign' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module '@babel/plugin-transform-object-assign' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module '@babel/plugin-transform-object-assign/lib' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module '@babel/plugin-transform-object-assign/lib/index' { 31 | declare module.exports: $Exports<'@babel/plugin-transform-object-assign/lib'>; 32 | } 33 | declare module '@babel/plugin-transform-object-assign/lib/index.js' { 34 | declare module.exports: $Exports<'@babel/plugin-transform-object-assign/lib'>; 35 | } 36 | -------------------------------------------------------------------------------- /flow-typed/npm/@babel/preset-flow_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 38cf00e2aa5f45239d4d90d87957e8f3 2 | // flow-typed version: <>/@babel/preset-flow_v^7.8.3/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * '@babel/preset-flow' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module '@babel/preset-flow' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module '@babel/preset-flow/lib' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module '@babel/preset-flow/lib/index' { 31 | declare module.exports: $Exports<'@babel/preset-flow/lib'>; 32 | } 33 | declare module '@babel/preset-flow/lib/index.js' { 34 | declare module.exports: $Exports<'@babel/preset-flow/lib'>; 35 | } 36 | -------------------------------------------------------------------------------- /flow-typed/npm/@babel/preset-react_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 167390b2c580ca10695cac64f2c42113 2 | // flow-typed version: <>/@babel/preset-react_v^7.8.3/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * '@babel/preset-react' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module '@babel/preset-react' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module '@babel/preset-react/lib' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module '@babel/preset-react/lib/index' { 31 | declare module.exports: $Exports<'@babel/preset-react/lib'>; 32 | } 33 | declare module '@babel/preset-react/lib/index.js' { 34 | declare module.exports: $Exports<'@babel/preset-react/lib'>; 35 | } 36 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-core_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 38a9f796011f49b3d1a03bf8b2a9de1b 2 | // flow-typed version: <>/babel-core_v^7.0.0-bridge.0/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-core' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-core' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | 26 | 27 | // Filename aliases 28 | declare module 'babel-core/index' { 29 | declare module.exports: $Exports<'babel-core'>; 30 | } 31 | declare module 'babel-core/index.js' { 32 | declare module.exports: $Exports<'babel-core'>; 33 | } 34 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-jest_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 2c1aa1e92b7497568f30f639b5c77ac5 2 | // flow-typed version: <>/babel-jest_v^25.1.0/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-jest' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-jest' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-jest/build' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-jest/build/index' { 31 | declare module.exports: $Exports<'babel-jest/build'>; 32 | } 33 | declare module 'babel-jest/build/index.js' { 34 | declare module.exports: $Exports<'babel-jest/build'>; 35 | } 36 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-dev-expression_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f7a859a0f3359b3dda3451326996c333 2 | // flow-typed version: <>/babel-plugin-dev-expression_v^0.2.2/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-dev-expression' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-dev-expression' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-dev-expression/dev-expression' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-dev-expression/dev-expression.js' { 31 | declare module.exports: $Exports<'babel-plugin-dev-expression/dev-expression'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-plugin-prettier_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 912ade96a1dba898160e27d7047a7b6a 2 | // flow-typed version: <>/eslint-plugin-prettier_v^3.1.2/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-plugin-prettier' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-plugin-prettier' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-plugin-prettier/eslint-plugin-prettier' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'eslint-plugin-prettier/eslint-plugin-prettier.js' { 31 | declare module.exports: $Exports<'eslint-plugin-prettier/eslint-plugin-prettier'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 28fdff7f110e1c75efab63ff205dda30 2 | // flow-typed version: c6154227d1/flow-bin_v0.x.x/flow_>=v0.104.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/jest-axe_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: aa29ce2ae2ecea0ec9f3c1f57fdf520a 2 | // flow-typed version: <>/jest-axe_v^3.3.0/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'jest-axe' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'jest-axe' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'jest-axe/extend-expect' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'jest-axe/extend-expect.js' { 31 | declare module.exports: $Exports<'jest-axe/extend-expect'>; 32 | } 33 | declare module 'jest-axe/index' { 34 | declare module.exports: $Exports<'jest-axe'>; 35 | } 36 | declare module 'jest-axe/index.js' { 37 | declare module.exports: $Exports<'jest-axe'>; 38 | } 39 | -------------------------------------------------------------------------------- /flow-typed/npm/require-from-string_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ad19217fac230ec2c67d4afe3b4645d1 2 | // flow-typed version: <>/require-from-string_v^2.0.2/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'require-from-string' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'require-from-string' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | 26 | 27 | // Filename aliases 28 | declare module 'require-from-string/index' { 29 | declare module.exports: $Exports<'require-from-string'>; 30 | } 31 | declare module 'require-from-string/index.js' { 32 | declare module.exports: $Exports<'require-from-string'>; 33 | } 34 | -------------------------------------------------------------------------------- /flow-typed/npm/rimraf_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 4f9dfefdfd54d755a43c1cc3824fc488 2 | // flow-typed version: <>/rimraf_v^3.0.1/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'rimraf' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'rimraf' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'rimraf/bin' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'rimraf/rimraf' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module 'rimraf/bin.js' { 35 | declare module.exports: $Exports<'rimraf/bin'>; 36 | } 37 | declare module 'rimraf/rimraf.js' { 38 | declare module.exports: $Exports<'rimraf/rimraf'>; 39 | } 40 | -------------------------------------------------------------------------------- /flow-typed/npm/rollup-plugin-terser_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: a7c8c8c0d56b7a709d5d48f26d97ff9f 2 | // flow-typed version: <>/rollup-plugin-terser_v^5.2.0/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'rollup-plugin-terser' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'rollup-plugin-terser' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'rollup-plugin-terser/transform' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'rollup-plugin-terser/index' { 31 | declare module.exports: $Exports<'rollup-plugin-terser'>; 32 | } 33 | declare module 'rollup-plugin-terser/index.js' { 34 | declare module.exports: $Exports<'rollup-plugin-terser'>; 35 | } 36 | declare module 'rollup-plugin-terser/transform.js' { 37 | declare module.exports: $Exports<'rollup-plugin-terser/transform'>; 38 | } 39 | -------------------------------------------------------------------------------- /flow-typed/npm/stylelint-config-recommended_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 4a539e33d88cdab1af3b2186bc03cecb 2 | // flow-typed version: <>/stylelint-config-recommended_v^3.0.0/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'stylelint-config-recommended' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'stylelint-config-recommended' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | 26 | 27 | // Filename aliases 28 | declare module 'stylelint-config-recommended/index' { 29 | declare module.exports: $Exports<'stylelint-config-recommended'>; 30 | } 31 | declare module 'stylelint-config-recommended/index.js' { 32 | declare module.exports: $Exports<'stylelint-config-recommended'>; 33 | } 34 | -------------------------------------------------------------------------------- /flow-typed/npm/stylelint-config-standard_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 889ab190baf52758e1e0eb8ed818929a 2 | // flow-typed version: <>/stylelint-config-standard_v^19.0.0/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'stylelint-config-standard' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'stylelint-config-standard' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | 26 | 27 | // Filename aliases 28 | declare module 'stylelint-config-standard/index' { 29 | declare module.exports: $Exports<'stylelint-config-standard'>; 30 | } 31 | declare module 'stylelint-config-standard/index.js' { 32 | declare module.exports: $Exports<'stylelint-config-standard'>; 33 | } 34 | -------------------------------------------------------------------------------- /flow-typed/npm/stylelint-config-styled-components_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: dd1609d786c47183d228636ef55ba6e3 2 | // flow-typed version: <>/stylelint-config-styled-components_v^0.1.1/flow_v0.110.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'stylelint-config-styled-components' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'stylelint-config-styled-components' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | 26 | 27 | // Filename aliases 28 | declare module 'stylelint-config-styled-components/index' { 29 | declare module.exports: $Exports<'stylelint-config-styled-components'>; 30 | } 31 | declare module 'stylelint-config-styled-components/index.js' { 32 | declare module.exports: $Exports<'stylelint-config-styled-components'>; 33 | } 34 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | 3 | module.exports = { 4 | setupFiles: [ 5 | // for some painful reason this is needed for our 'async' usage 6 | // in drop-dev-warnings-for-prod.spec.js 7 | require.resolve('regenerator-runtime/runtime'), 8 | './test/env-setup.js', 9 | ], 10 | setupFilesAfterEnv: ['./test/test-setup.js'], 11 | // node_modules is default. 12 | testPathIgnorePatterns: ['/node_modules/', '/cypress/'], 13 | modulePathIgnorePatterns: ['/dist/'], 14 | watchPlugins: [ 15 | 'jest-watch-typeahead/filename', 16 | 'jest-watch-typeahead/testname', 17 | ], 18 | verbose: true, 19 | }; 20 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /server-ports.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const ports = { 3 | storybook: 9002, 4 | cspServer: 9003, 5 | }; 6 | 7 | module.exports = ports; 8 | -------------------------------------------------------------------------------- /src/debug/middleware/action-timing.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable no-console */ 3 | import * as timings from '../timings'; 4 | import type { Action } from '../../state/store-types'; 5 | 6 | export default () => (next: (Action) => mixed) => (action: Action): any => { 7 | timings.forceEnable(); 8 | const key = `redux action: ${action.type}`; 9 | timings.start(key); 10 | 11 | const result: mixed = next(action); 12 | 13 | timings.finish(key); 14 | 15 | return result; 16 | }; 17 | -------------------------------------------------------------------------------- /src/debug/middleware/log.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable no-console */ 3 | import type { Action, Store } from '../../state/store-types'; 4 | 5 | type Mode = 'verbose' | 'light'; 6 | 7 | export default (mode?: Mode = 'verbose') => (store: Store) => ( 8 | next: (Action) => mixed, 9 | ) => (action: Action): any => { 10 | if (mode === 'light') { 11 | console.log('🏃‍ Action:', action.type); 12 | return next(action); 13 | } 14 | 15 | console.group(`action: ${action.type}`); 16 | console.log('action payload', action.payload); 17 | 18 | console.log('state before', store.getState()); 19 | 20 | const result: mixed = next(action); 21 | 22 | console.log('state after', store.getState()); 23 | console.groupEnd(); 24 | 25 | return result; 26 | }; 27 | -------------------------------------------------------------------------------- /src/debug/middleware/user-timing.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action } from '../../state/store-types'; 3 | 4 | export default () => (next: (Action) => mixed) => (action: Action): any => { 5 | const title: string = `👾 redux (action): ${action.type}`; 6 | const startMark: string = `${action.type}:start`; 7 | const endMark: string = `${action.type}:end`; 8 | 9 | performance.mark(startMark); 10 | const result: mixed = next(action); 11 | performance.mark(endMark); 12 | 13 | performance.measure(title, startMark, endMark); 14 | 15 | return result; 16 | }; 17 | -------------------------------------------------------------------------------- /src/empty.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export function noop(): void {} 3 | 4 | export function identity(value: T): T { 5 | return value; 6 | } 7 | -------------------------------------------------------------------------------- /src/invariant.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable no-restricted-syntax */ 3 | const isProduction: boolean = process.env.NODE_ENV === 'production'; 4 | const prefix: string = 'Invariant failed'; 5 | 6 | // Want to use this: 7 | // export class RbdInvariant extends Error { } 8 | // But it causes babel to bring in a lot of code 9 | 10 | export function RbdInvariant(message: string) { 11 | this.message = message; 12 | } 13 | // $FlowFixMe 14 | RbdInvariant.prototype.toString = function toString() { 15 | return this.message; 16 | }; 17 | 18 | // A copy-paste of tiny-invariant but with a custom error type 19 | // Throw an error if the condition fails 20 | export function invariant(condition: mixed, message?: string) { 21 | if (condition) { 22 | return; 23 | } 24 | 25 | if (isProduction) { 26 | // In production we strip the message but still throw 27 | throw new RbdInvariant(prefix); 28 | } else { 29 | // When not in production we allow the message to pass through 30 | // *This block will be removed in production builds* 31 | throw new RbdInvariant(`${prefix}: ${message || ''}`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/state/auto-scroller/auto-scroller-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { State, DraggingState } from '../../types'; 3 | 4 | export type AutoScroller = {| 5 | start: (state: DraggingState) => void, 6 | stop: () => void, 7 | scroll: (state: State) => void, 8 | |}; 9 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/config.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // Values used to control how the fluid auto scroll feels 4 | const config = { 5 | // percentage distance from edge of container: 6 | startFromPercentage: 0.25, 7 | maxScrollAtPercentage: 0.05, 8 | // pixels per frame 9 | maxPixelScroll: 28, 10 | 11 | // A function used to ease a percentage value 12 | // A simple linear function would be: (percentage) => percentage; 13 | // percentage is between 0 and 1 14 | // result must be between 0 and 1 15 | ease: (percentage: number): number => Math.pow(percentage, 2), 16 | 17 | durationDampening: { 18 | // ms: how long to dampen the speed of an auto scroll from the start of a drag 19 | stopDampeningAt: 1200, 20 | // ms: when to start accelerating the reduction of duration dampening 21 | accelerateAt: 360, 22 | }, 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/did-start-in-scrollable-area.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/get-droppable-scroll-change.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position, Rect } from 'css-box-model'; 3 | import type { Scrollable, DroppableDimension } from '../../../types'; 4 | import getScroll from './get-scroll'; 5 | import { canScrollDroppable } from '../can-scroll'; 6 | 7 | type Args = {| 8 | droppable: DroppableDimension, 9 | subject: Rect, 10 | center: Position, 11 | dragStartTime: number, 12 | shouldUseTimeDampening: boolean, 13 | |}; 14 | 15 | export default ({ 16 | droppable, 17 | subject, 18 | center, 19 | dragStartTime, 20 | shouldUseTimeDampening, 21 | }: Args): ?Position => { 22 | // We know this has a closestScrollable 23 | const frame: ?Scrollable = droppable.frame; 24 | 25 | // this should never happen - just being safe 26 | if (!frame) { 27 | return null; 28 | } 29 | 30 | const scroll: ?Position = getScroll({ 31 | dragStartTime, 32 | container: frame.pageMarginBox, 33 | subject, 34 | center, 35 | shouldUseTimeDampening, 36 | }); 37 | 38 | return scroll && canScrollDroppable(droppable, scroll) ? scroll : null; 39 | }; 40 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/get-percentage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { warning } from '../../../dev-warning'; 3 | 4 | type Args = {| 5 | startOfRange: number, 6 | endOfRange: number, 7 | current: number, 8 | |}; 9 | 10 | export default ({ startOfRange, endOfRange, current }: Args): number => { 11 | const range: number = endOfRange - startOfRange; 12 | 13 | if (range === 0) { 14 | warning(` 15 | Detected distance range of 0 in the fluid auto scroller 16 | This is unexpected and would cause a divide by 0 issue. 17 | Not allowing an auto scroll 18 | `); 19 | return 0; 20 | } 21 | 22 | const currentInRange: number = current - startOfRange; 23 | const percentage: number = currentInRange / range; 24 | return percentage; 25 | }; 26 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/get-scroll/adjust-for-size-limits.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Rect, Position } from 'css-box-model'; 3 | 4 | type Args = {| 5 | container: Rect, 6 | subject: Rect, 7 | proposedScroll: Position, 8 | |}; 9 | 10 | export default ({ container, subject, proposedScroll }: Args): ?Position => { 11 | const isTooBigVertically: boolean = subject.height > container.height; 12 | const isTooBigHorizontally: boolean = subject.width > container.width; 13 | 14 | // not too big on any axis 15 | if (!isTooBigHorizontally && !isTooBigVertically) { 16 | return proposedScroll; 17 | } 18 | 19 | // too big on both axis 20 | if (isTooBigHorizontally && isTooBigVertically) { 21 | return null; 22 | } 23 | 24 | // Only too big on one axis 25 | // Exclude the axis that we cannot scroll on 26 | return { 27 | x: isTooBigHorizontally ? 0 : proposedScroll.x, 28 | y: isTooBigVertically ? 0 : proposedScroll.y, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/get-scroll/get-scroll-on-axis/dampen-value-by-time.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getPercentage from '../../get-percentage'; 3 | import config from '../../config'; 4 | import minScroll from './min-scroll'; 5 | 6 | const accelerateAt: number = config.durationDampening.accelerateAt; 7 | const stopAt: number = config.durationDampening.stopDampeningAt; 8 | 9 | export default (proposedScroll: number, dragStartTime: number): number => { 10 | const startOfRange: number = dragStartTime; 11 | const endOfRange: number = stopAt; 12 | const now: number = Date.now(); 13 | const runTime: number = now - startOfRange; 14 | 15 | // we have finished the time dampening period 16 | if (runTime >= stopAt) { 17 | return proposedScroll; 18 | } 19 | 20 | // Up to this point we know there is a proposed scroll 21 | // but we have not reached our accelerate point 22 | // Return the minimum amount of scroll 23 | if (runTime < accelerateAt) { 24 | return minScroll; 25 | } 26 | 27 | const betweenAccelerateAtAndStopAtPercentage: number = getPercentage({ 28 | startOfRange: accelerateAt, 29 | endOfRange, 30 | current: runTime, 31 | }); 32 | 33 | const scroll: number = 34 | proposedScroll * config.ease(betweenAccelerateAtAndStopAtPercentage); 35 | 36 | return Math.ceil(scroll); 37 | }; 38 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/get-scroll/get-scroll-on-axis/get-distance-thresholds.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Rect } from 'css-box-model'; 3 | import config from '../../config'; 4 | import type { Axis } from '../../../../../types'; 5 | 6 | // all in pixels 7 | export type DistanceThresholds = {| 8 | startScrollingFrom: number, 9 | maxScrollValueAt: number, 10 | |}; 11 | 12 | // converts the percentages in the config into actual pixel values 13 | export default (container: Rect, axis: Axis): DistanceThresholds => { 14 | const startScrollingFrom: number = 15 | container[axis.size] * config.startFromPercentage; 16 | const maxScrollValueAt: number = 17 | container[axis.size] * config.maxScrollAtPercentage; 18 | 19 | const thresholds: DistanceThresholds = { 20 | startScrollingFrom, 21 | maxScrollValueAt, 22 | }; 23 | 24 | return thresholds; 25 | }; 26 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/get-scroll/get-scroll-on-axis/get-value.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type DistanceThresholds } from './get-distance-thresholds'; 3 | import getValueFromDistance from './get-value-from-distance'; 4 | import dampenValueByTime from './dampen-value-by-time'; 5 | import minScroll from './min-scroll'; 6 | 7 | type Args = {| 8 | distanceToEdge: number, 9 | thresholds: DistanceThresholds, 10 | dragStartTime: number, 11 | shouldUseTimeDampening: boolean, 12 | |}; 13 | 14 | export default ({ 15 | distanceToEdge, 16 | thresholds, 17 | dragStartTime, 18 | shouldUseTimeDampening, 19 | }: Args): number => { 20 | const scroll: number = getValueFromDistance(distanceToEdge, thresholds); 21 | 22 | // not enough distance to trigger a minimum scroll 23 | // we can bail here 24 | if (scroll === 0) { 25 | return 0; 26 | } 27 | 28 | // Dampen an auto scroll speed based on duration of drag 29 | 30 | if (!shouldUseTimeDampening) { 31 | return scroll; 32 | } 33 | 34 | // Once we know an auto scroll should occur based on distance, 35 | // we must let at least 1px through to trigger a scroll event an 36 | // another auto scroll call 37 | 38 | return Math.max(dampenValueByTime(scroll, dragStartTime), minScroll); 39 | }; 40 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/get-scroll/get-scroll-on-axis/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Rect, Spacing } from 'css-box-model'; 3 | import getDistanceThresholds, { 4 | type DistanceThresholds, 5 | } from './get-distance-thresholds'; 6 | import type { Axis } from '../../../../../types'; 7 | import getValue from './get-value'; 8 | 9 | type GetOnAxisArgs = {| 10 | container: Rect, 11 | distanceToEdges: Spacing, 12 | dragStartTime: number, 13 | axis: Axis, 14 | shouldUseTimeDampening: boolean, 15 | |}; 16 | 17 | export default ({ 18 | container, 19 | distanceToEdges, 20 | dragStartTime, 21 | axis, 22 | shouldUseTimeDampening, 23 | }: GetOnAxisArgs): number => { 24 | const thresholds: DistanceThresholds = getDistanceThresholds(container, axis); 25 | const isCloserToEnd: boolean = 26 | distanceToEdges[axis.end] < distanceToEdges[axis.start]; 27 | 28 | if (isCloserToEnd) { 29 | return getValue({ 30 | distanceToEdge: distanceToEdges[axis.end], 31 | thresholds, 32 | dragStartTime, 33 | shouldUseTimeDampening, 34 | }); 35 | } 36 | 37 | return ( 38 | -1 * 39 | getValue({ 40 | distanceToEdge: distanceToEdges[axis.start], 41 | thresholds, 42 | dragStartTime, 43 | shouldUseTimeDampening, 44 | }) 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/get-scroll/get-scroll-on-axis/min-scroll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // A scroll event will only be triggered when there is a value of at least 1px change 4 | export default 1; 5 | -------------------------------------------------------------------------------- /src/state/auto-scroller/fluid-scroller/get-window-scroll-change.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position, Rect } from 'css-box-model'; 3 | import type { Viewport } from '../../../types'; 4 | import getScroll from './get-scroll'; 5 | import { canScrollWindow } from '../can-scroll'; 6 | 7 | type Args = {| 8 | viewport: Viewport, 9 | subject: Rect, 10 | center: Position, 11 | dragStartTime: number, 12 | shouldUseTimeDampening: boolean, 13 | |}; 14 | 15 | export default ({ 16 | viewport, 17 | subject, 18 | center, 19 | dragStartTime, 20 | shouldUseTimeDampening, 21 | }: Args): ?Position => { 22 | const scroll: ?Position = getScroll({ 23 | dragStartTime, 24 | container: viewport.frame, 25 | subject, 26 | center, 27 | shouldUseTimeDampening, 28 | }); 29 | 30 | return scroll && canScrollWindow(viewport, scroll) ? scroll : null; 31 | }; 32 | -------------------------------------------------------------------------------- /src/state/axis.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { HorizontalAxis, VerticalAxis } from '../types'; 3 | 4 | export const vertical: VerticalAxis = { 5 | direction: 'vertical', 6 | line: 'y', 7 | crossAxisLine: 'x', 8 | start: 'top', 9 | end: 'bottom', 10 | size: 'height', 11 | crossAxisStart: 'left', 12 | crossAxisEnd: 'right', 13 | crossAxisSize: 'width', 14 | }; 15 | 16 | export const horizontal: HorizontalAxis = { 17 | direction: 'horizontal', 18 | line: 'x', 19 | crossAxisLine: 'y', 20 | start: 'left', 21 | end: 'right', 22 | size: 'width', 23 | crossAxisStart: 'top', 24 | crossAxisEnd: 'bottom', 25 | crossAxisSize: 'height', 26 | }; 27 | -------------------------------------------------------------------------------- /src/state/can-start-drag.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { State, DraggableId } from '../types'; 3 | 4 | export default (state: State, id: DraggableId): boolean => { 5 | // Ready to go! 6 | if (state.phase === 'IDLE') { 7 | return true; 8 | } 9 | 10 | // Can lift depending on the type of drop animation 11 | if (state.phase !== 'DROP_ANIMATING') { 12 | return false; 13 | } 14 | 15 | // - For a user drop we allow the user to drag other Draggables 16 | // immediately as items are most likely already in their home 17 | // - For a cancel items will be moving back to their original position 18 | // as such it is a cleaner experience to block them from dragging until 19 | // the drop animation is complete. Otherwise they will be grabbing 20 | // items not in their original position which can lead to bad visuals 21 | // Not allowing dragging of the dropping draggable 22 | if (state.completed.result.draggableId === id) { 23 | return false; 24 | } 25 | 26 | // if dropping - allow lifting 27 | // if cancelling - disallow lifting 28 | return state.completed.result.reason === 'DROP'; 29 | }; 30 | -------------------------------------------------------------------------------- /src/state/did-start-after-critical.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DraggableId, LiftEffect } from '../types'; 3 | 4 | export default function didStartAfterCritical( 5 | draggableId: DraggableId, 6 | afterCritical: LiftEffect, 7 | ): boolean { 8 | return Boolean(afterCritical.effected[draggableId]); 9 | } 10 | -------------------------------------------------------------------------------- /src/state/dimension-structures.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import memoizeOne from 'memoize-one'; 3 | import { values } from '../native-with-fallback'; 4 | import type { 5 | DroppableDimension, 6 | DroppableDimensionMap, 7 | DraggableDimension, 8 | DraggableDimensionMap, 9 | } from '../types'; 10 | 11 | export const toDroppableMap = memoizeOne( 12 | (droppables: DroppableDimension[]): DroppableDimensionMap => 13 | droppables.reduce((previous, current) => { 14 | previous[current.descriptor.id] = current; 15 | return previous; 16 | }, {}), 17 | ); 18 | 19 | export const toDraggableMap = memoizeOne( 20 | (draggables: DraggableDimension[]): DraggableDimensionMap => 21 | draggables.reduce((previous, current) => { 22 | previous[current.descriptor.id] = current; 23 | return previous; 24 | }, {}), 25 | ); 26 | 27 | export const toDroppableList = memoizeOne( 28 | (droppables: DroppableDimensionMap): DroppableDimension[] => 29 | values(droppables), 30 | ); 31 | 32 | export const toDraggableList = memoizeOne( 33 | (draggables: DraggableDimensionMap): DraggableDimension[] => 34 | values(draggables), 35 | ); 36 | -------------------------------------------------------------------------------- /src/state/droppable/is-home-of.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DraggableDimension, DroppableDimension } from '../../types'; 3 | 4 | export default ( 5 | draggable: DraggableDimension, 6 | destination: DroppableDimension, 7 | ): boolean => draggable.descriptor.droppableId === destination.descriptor.id; 8 | -------------------------------------------------------------------------------- /src/state/droppable/should-use-placeholder.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DraggableDescriptor, DragImpact } from '../../types'; 3 | import whatIsDraggedOver from './what-is-dragged-over'; 4 | 5 | // use placeholder if dragged over 6 | export default (descriptor: DraggableDescriptor, impact: DragImpact): boolean => 7 | whatIsDraggedOver(impact) === descriptor.droppableId; 8 | -------------------------------------------------------------------------------- /src/state/droppable/util/clip.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getRect, type Rect, type Spacing } from 'css-box-model'; 3 | 4 | export default (frame: Spacing, subject: Spacing): ?Rect => { 5 | const result: Rect = getRect({ 6 | top: Math.max(subject.top, frame.top), 7 | right: Math.min(subject.right, frame.right), 8 | bottom: Math.min(subject.bottom, frame.bottom), 9 | left: Math.max(subject.left, frame.left), 10 | }); 11 | 12 | if (result.width <= 0 || result.height <= 0) { 13 | return null; 14 | } 15 | 16 | return result; 17 | }; 18 | -------------------------------------------------------------------------------- /src/state/droppable/what-is-dragged-over-from-result.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DroppableId, DropResult } from '../../types'; 3 | 4 | export default (result: DropResult): ?DroppableId => { 5 | const { combine, destination } = result; 6 | 7 | if (destination) { 8 | return destination.droppableId; 9 | } 10 | 11 | if (combine) { 12 | return combine.droppableId; 13 | } 14 | 15 | return null; 16 | }; 17 | -------------------------------------------------------------------------------- /src/state/droppable/what-is-dragged-over.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ImpactLocation, DroppableId, DragImpact } from '../../types'; 3 | 4 | export default (impact: DragImpact): ?DroppableId => { 5 | const at: ?ImpactLocation = impact.at; 6 | 7 | if (!at) { 8 | return null; 9 | } 10 | 11 | if (at.type === 'REORDER') { 12 | return at.destination.droppableId; 13 | } 14 | 15 | return at.combine.droppableId; 16 | }; 17 | -------------------------------------------------------------------------------- /src/state/get-center-from-impact/get-client-border-box-center/get-client-from-page-border-box-center.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { Viewport, DraggableDimension } from '../../../types'; 4 | import { add, subtract } from '../../position'; 5 | import withViewportDisplacement from '../../with-scroll-change/with-viewport-displacement'; 6 | 7 | type Args = {| 8 | pageBorderBoxCenter: Position, 9 | draggable: DraggableDimension, 10 | viewport: Viewport, 11 | |}; 12 | 13 | export default ({ 14 | pageBorderBoxCenter, 15 | draggable, 16 | viewport, 17 | }: Args): Position => { 18 | const withoutPageScrollChange: Position = withViewportDisplacement( 19 | viewport, 20 | pageBorderBoxCenter, 21 | ); 22 | 23 | const offset: Position = subtract( 24 | withoutPageScrollChange, 25 | draggable.page.borderBox.center, 26 | ); 27 | 28 | return add(draggable.client.borderBox.center, offset); 29 | }; 30 | -------------------------------------------------------------------------------- /src/state/get-center-from-impact/get-client-border-box-center/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { 4 | DroppableDimension, 5 | Viewport, 6 | DragImpact, 7 | DraggableDimension, 8 | DraggableDimensionMap, 9 | LiftEffect, 10 | } from '../../../types'; 11 | import getPageBorderBoxCenterFromImpact from '../get-page-border-box-center'; 12 | import getClientFromPageBorderBoxCenter from './get-client-from-page-border-box-center'; 13 | 14 | type Args = {| 15 | impact: DragImpact, 16 | draggable: DraggableDimension, 17 | droppable: DroppableDimension, 18 | draggables: DraggableDimensionMap, 19 | viewport: Viewport, 20 | afterCritical: LiftEffect, 21 | |}; 22 | 23 | export default ({ 24 | impact, 25 | draggable, 26 | droppable, 27 | draggables, 28 | viewport, 29 | afterCritical, 30 | }: Args): Position => { 31 | const pageBorderBoxCenter: Position = getPageBorderBoxCenterFromImpact({ 32 | impact, 33 | draggable, 34 | draggables, 35 | droppable, 36 | afterCritical, 37 | }); 38 | 39 | return getClientFromPageBorderBoxCenter({ 40 | pageBorderBoxCenter, 41 | draggable, 42 | viewport, 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/state/get-combined-item-displacement.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { 4 | DisplacementGroups, 5 | LiftEffect, 6 | DraggableId, 7 | DisplacedBy, 8 | } from '../types'; 9 | import { origin, negate } from './position'; 10 | import didStartAfterCritical from './did-start-after-critical'; 11 | 12 | type Args = {| 13 | displaced: DisplacementGroups, 14 | afterCritical: LiftEffect, 15 | combineWith: DraggableId, 16 | displacedBy: DisplacedBy, 17 | |}; 18 | 19 | export default ({ 20 | displaced, 21 | afterCritical, 22 | combineWith, 23 | displacedBy, 24 | }: Args): Position => { 25 | const isDisplaced: boolean = Boolean( 26 | displaced.visible[combineWith] || displaced.invisible[combineWith], 27 | ); 28 | 29 | if (didStartAfterCritical(combineWith, afterCritical)) { 30 | return isDisplaced ? origin : negate(displacedBy.point); 31 | } 32 | 33 | return isDisplaced ? displacedBy.point : origin; 34 | }; 35 | -------------------------------------------------------------------------------- /src/state/get-displaced-by.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import memoizeOne from 'memoize-one'; 3 | import { type Position } from 'css-box-model'; 4 | import type { Axis, DisplacedBy } from '../types'; 5 | import { patch } from './position'; 6 | 7 | // TODO: memoization needed? 8 | export default memoizeOne(function getDisplacedBy( 9 | axis: Axis, 10 | displaceBy: Position, 11 | ): DisplacedBy { 12 | const displacement: number = displaceBy[axis.line]; 13 | return { 14 | value: displacement, 15 | point: patch(axis.line, displacement), 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /src/state/get-draggables-inside-droppable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import memoizeOne from 'memoize-one'; 3 | import { toDraggableList } from './dimension-structures'; 4 | import type { 5 | DraggableDimension, 6 | DroppableId, 7 | DraggableDimensionMap, 8 | } from '../types'; 9 | 10 | export default memoizeOne( 11 | ( 12 | // using droppableId to avoid cache busted when we 13 | // update the droppable (such as when it scrolls) 14 | droppableId: DroppableId, 15 | draggables: DraggableDimensionMap, 16 | ): DraggableDimension[] => { 17 | const result = toDraggableList(draggables) 18 | .filter( 19 | (draggable: DraggableDimension): boolean => 20 | droppableId === draggable.descriptor.droppableId, 21 | ) 22 | // Dimensions are not guarenteed to be ordered in the same order as keys 23 | // So we need to sort them so they are in the correct order 24 | .sort( 25 | (a: DraggableDimension, b: DraggableDimension): number => 26 | a.descriptor.index - b.descriptor.index, 27 | ); 28 | 29 | return result; 30 | }, 31 | ); 32 | -------------------------------------------------------------------------------- /src/state/get-frame.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { invariant } from '../invariant'; 3 | 4 | import type { DroppableDimension, Scrollable } from '../types'; 5 | 6 | export default (droppable: DroppableDimension): Scrollable => { 7 | const frame: ?Scrollable = droppable.frame; 8 | invariant(frame, 'Expected Droppable to have a frame'); 9 | return frame; 10 | }; 11 | -------------------------------------------------------------------------------- /src/state/get-home-location.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DraggableDescriptor, DraggableLocation } from '../types'; 3 | 4 | export default (descriptor: DraggableDescriptor): DraggableLocation => ({ 5 | index: descriptor.index, 6 | droppableId: descriptor.droppableId, 7 | }); 8 | -------------------------------------------------------------------------------- /src/state/get-impact-location.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DragImpact, DraggableLocation, Combine } from '../types'; 3 | 4 | export function tryGetDestination(impact: DragImpact): ?DraggableLocation { 5 | if (impact.at && impact.at.type === 'REORDER') { 6 | return impact.at.destination; 7 | } 8 | return null; 9 | } 10 | 11 | export function tryGetCombine(impact: DragImpact): ?Combine { 12 | if (impact.at && impact.at.type === 'COMBINE') { 13 | return impact.at.combine; 14 | } 15 | return null; 16 | } 17 | -------------------------------------------------------------------------------- /src/state/get-is-displaced.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DisplacementGroups, DraggableId } from '../types'; 3 | 4 | type Args = {| 5 | displaced: DisplacementGroups, 6 | id: DraggableId, 7 | |}; 8 | 9 | export default function getIsDisplaced({ displaced, id }: Args): boolean { 10 | return Boolean(displaced.visible[id] || displaced.invisible[id]); 11 | } 12 | -------------------------------------------------------------------------------- /src/state/get-max-scroll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Position } from 'css-box-model'; 3 | import { subtract } from './position'; 4 | 5 | type Args = {| 6 | scrollHeight: number, 7 | scrollWidth: number, 8 | height: number, 9 | width: number, 10 | |}; 11 | export default ({ 12 | scrollHeight, 13 | scrollWidth, 14 | height, 15 | width, 16 | }: Args): Position => { 17 | const maxScroll: Position = subtract( 18 | // full size 19 | { x: scrollWidth, y: scrollHeight }, 20 | // viewport size 21 | { x: width, y: height }, 22 | ); 23 | 24 | const adjustedMaxScroll: Position = { 25 | x: Math.max(0, maxScroll.x), 26 | y: Math.max(0, maxScroll.y), 27 | }; 28 | 29 | return adjustedMaxScroll; 30 | }; 31 | -------------------------------------------------------------------------------- /src/state/is-movement-allowed.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { State } from '../types'; 3 | // Using function declaration as arrow function does not play well with the %checks syntax 4 | export default function isMovementAllowed(state: State): boolean %checks { 5 | return state.phase === 'DRAGGING' || state.phase === 'COLLECTING'; 6 | } 7 | -------------------------------------------------------------------------------- /src/state/is-within.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // is a value between two other values 4 | 5 | export default ( 6 | lowerBound: number, 7 | upperBound: number, 8 | ): ((number) => boolean) => (value: number): boolean => 9 | lowerBound <= value && value <= upperBound; 10 | -------------------------------------------------------------------------------- /src/state/middleware/auto-scroll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { invariant } from '../../invariant'; 3 | import type { AutoScroller } from '../auto-scroller/auto-scroller-types'; 4 | import type { Action, Dispatch, MiddlewareStore } from '../store-types'; 5 | import type { State } from '../../types'; 6 | 7 | const shouldStop = (action: Action): boolean => 8 | action.type === 'DROP_COMPLETE' || 9 | action.type === 'DROP_ANIMATE' || 10 | action.type === 'FLUSH'; 11 | 12 | export default (autoScroller: AutoScroller) => (store: MiddlewareStore) => ( 13 | next: Dispatch, 14 | ) => (action: Action): any => { 15 | if (shouldStop(action)) { 16 | autoScroller.stop(); 17 | next(action); 18 | return; 19 | } 20 | 21 | if (action.type === 'INITIAL_PUBLISH') { 22 | // letting the action go first to hydrate the state 23 | next(action); 24 | const state: State = store.getState(); 25 | invariant( 26 | state.phase === 'DRAGGING', 27 | 'Expected phase to be DRAGGING after INITIAL_PUBLISH', 28 | ); 29 | autoScroller.start(state); 30 | return; 31 | } 32 | 33 | // auto scroll happens in response to state changes 34 | // releasing all actions to the reducer first 35 | next(action); 36 | 37 | autoScroller.scroll(store.getState()); 38 | }; 39 | -------------------------------------------------------------------------------- /src/state/middleware/dimension-marshal-stopper.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action, Dispatch } from '../store-types'; 3 | import type { DimensionMarshal } from '../dimension-marshal/dimension-marshal-types'; 4 | 5 | export default (marshal: DimensionMarshal) => () => (next: Dispatch) => ( 6 | action: Action, 7 | ): any => { 8 | // Not stopping a collection on a 'DROP' as we want a collection to continue 9 | if ( 10 | // drag is finished 11 | action.type === 'DROP_COMPLETE' || 12 | action.type === 'FLUSH' || 13 | // no longer accepting changes once the drop has started 14 | action.type === 'DROP_ANIMATE' 15 | ) { 16 | marshal.stopPublishing(); 17 | } 18 | 19 | next(action); 20 | }; 21 | -------------------------------------------------------------------------------- /src/state/middleware/drop/drop-animation-finish-middleware.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { invariant } from '../../../invariant'; 3 | import { completeDrop } from '../../action-creators'; 4 | import type { State } from '../../../types'; 5 | import type { MiddlewareStore, Action, Dispatch } from '../../store-types'; 6 | 7 | export default (store: MiddlewareStore) => (next: Dispatch) => ( 8 | action: Action, 9 | ): any => { 10 | if (action.type !== 'DROP_ANIMATION_FINISHED') { 11 | next(action); 12 | return; 13 | } 14 | 15 | const state: State = store.getState(); 16 | invariant( 17 | state.phase === 'DROP_ANIMATING', 18 | 'Cannot finish a drop animating when no drop is occurring', 19 | ); 20 | store.dispatch(completeDrop({ completed: state.completed })); 21 | }; 22 | -------------------------------------------------------------------------------- /src/state/middleware/drop/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './drop-middleware'; 3 | -------------------------------------------------------------------------------- /src/state/middleware/focus.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DropResult } from '../../types'; 3 | import type { Action, Dispatch } from '../store-types'; 4 | import type { FocusMarshal } from '../../view/use-focus-marshal/focus-marshal-types'; 5 | 6 | export default (marshal: FocusMarshal) => { 7 | let isWatching: boolean = false; 8 | 9 | return () => (next: Dispatch) => (action: Action): any => { 10 | if (action.type === 'INITIAL_PUBLISH') { 11 | isWatching = true; 12 | 13 | marshal.tryRecordFocus(action.payload.critical.draggable.id); 14 | next(action); 15 | marshal.tryRestoreFocusRecorded(); 16 | return; 17 | } 18 | 19 | next(action); 20 | 21 | if (!isWatching) { 22 | return; 23 | } 24 | 25 | if (action.type === 'FLUSH') { 26 | isWatching = false; 27 | marshal.tryRestoreFocusRecorded(); 28 | return; 29 | } 30 | 31 | if (action.type === 'DROP_COMPLETE') { 32 | isWatching = false; 33 | const result: DropResult = action.payload.completed.result; 34 | 35 | // give focus to the combine target when combining 36 | if (result.combine) { 37 | marshal.tryShiftRecord(result.draggableId, result.combine.draggableId); 38 | } 39 | marshal.tryRestoreFocusRecorded(); 40 | } 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /src/state/middleware/pending-drop.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { drop } from '../action-creators'; 3 | import type { State } from '../../types'; 4 | import type { MiddlewareStore, Dispatch, Action } from '../store-types'; 5 | 6 | export default (store: MiddlewareStore) => (next: Dispatch) => ( 7 | action: Action, 8 | ): any => { 9 | // Always let the action go through first 10 | next(action); 11 | 12 | if (action.type !== 'PUBLISH_WHILE_DRAGGING') { 13 | return; 14 | } 15 | 16 | // A bulk replace occurred - check if 17 | // 1. there is a pending drop 18 | // 2. that the pending drop is no longer waiting 19 | 20 | const postActionState: State = store.getState(); 21 | 22 | // no pending drop after the publish 23 | if (postActionState.phase !== 'DROP_PENDING') { 24 | return; 25 | } 26 | 27 | // the pending drop is still waiting for completion 28 | if (postActionState.isWaiting) { 29 | return; 30 | } 31 | 32 | store.dispatch( 33 | drop({ 34 | reason: postActionState.reason, 35 | }), 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/state/middleware/responders/expiring-announce.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Announce } from '../../../types'; 3 | import { warning } from '../../../dev-warning'; 4 | 5 | export type ExpiringAnnounce = Announce & { 6 | wasCalled: () => boolean, 7 | }; 8 | 9 | export default (announce: Announce): ExpiringAnnounce => { 10 | let wasCalled: boolean = false; 11 | let isExpired: boolean = false; 12 | 13 | // not allowing async announcements 14 | const timeoutId: TimeoutID = setTimeout(() => { 15 | isExpired = true; 16 | }); 17 | 18 | const result = (message: string): void => { 19 | if (wasCalled) { 20 | warning('Announcement already made. Not making a second announcement'); 21 | 22 | return; 23 | } 24 | 25 | if (isExpired) { 26 | warning(` 27 | Announcements cannot be made asynchronously. 28 | Default message has already been announced. 29 | `); 30 | return; 31 | } 32 | 33 | wasCalled = true; 34 | announce(message); 35 | clearTimeout(timeoutId); 36 | }; 37 | 38 | // getter for isExpired 39 | // using this technique so that a consumer cannot 40 | // set the isExpired or wasCalled flags 41 | result.wasCalled = (): boolean => wasCalled; 42 | 43 | return result; 44 | }; 45 | -------------------------------------------------------------------------------- /src/state/middleware/responders/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './responders-middleware'; 3 | -------------------------------------------------------------------------------- /src/state/middleware/scroll-listener.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import { moveByWindowScroll } from '../action-creators'; 4 | import type { MiddlewareStore, Action, Dispatch } from '../store-types'; 5 | import getScrollListener from '../../view/scroll-listener'; 6 | 7 | // TODO: this is taken from auto-scroll. Let's make it a util 8 | const shouldEnd = (action: Action): boolean => 9 | action.type === 'DROP_COMPLETE' || 10 | action.type === 'DROP_ANIMATE' || 11 | action.type === 'FLUSH'; 12 | 13 | export default (store: MiddlewareStore) => { 14 | const listener = getScrollListener({ 15 | onWindowScroll: (newScroll: Position) => { 16 | store.dispatch(moveByWindowScroll({ newScroll })); 17 | }, 18 | }); 19 | 20 | return (next: Dispatch) => (action: Action): any => { 21 | if (!listener.isActive() && action.type === 'INITIAL_PUBLISH') { 22 | listener.start(); 23 | } 24 | 25 | if (listener.isActive() && shouldEnd(action)) { 26 | listener.stop(); 27 | } 28 | 29 | next(action); 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/state/middleware/style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action, Dispatch } from '../store-types'; 3 | import type { StyleMarshal } from '../../view/use-style-marshal/style-marshal-types'; 4 | 5 | export default (marshal: StyleMarshal) => () => (next: Dispatch) => ( 6 | action: Action, 7 | ): any => { 8 | if (action.type === 'INITIAL_PUBLISH') { 9 | marshal.dragging(); 10 | } 11 | 12 | if (action.type === 'DROP_ANIMATE') { 13 | marshal.dropping(action.payload.completed.result.reason); 14 | } 15 | 16 | // this will clear any styles immediately before a reorder 17 | if (action.type === 'FLUSH' || action.type === 'DROP_COMPLETE') { 18 | marshal.resting(); 19 | } 20 | 21 | next(action); 22 | }; 23 | -------------------------------------------------------------------------------- /src/state/move-in-direction/move-cross-axis/without-starting-displacement.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position, Rect, Spacing } from 'css-box-model'; 3 | import type { DraggableDimension, LiftEffect } from '../../../types'; 4 | import { negate, subtract } from '../../position'; 5 | import { offsetByPosition } from '../../spacing'; 6 | import didStartAfterCritical from '../../did-start-after-critical'; 7 | 8 | export const getCurrentPageBorderBoxCenter = ( 9 | draggable: DraggableDimension, 10 | afterCritical: LiftEffect, 11 | ): Position => { 12 | // If an item started displaced it is now resting 13 | // in a non-displaced location 14 | const original: Position = draggable.page.borderBox.center; 15 | return didStartAfterCritical(draggable.descriptor.id, afterCritical) 16 | ? subtract(original, afterCritical.displacedBy.point) 17 | : original; 18 | }; 19 | 20 | export const getCurrentPageBorderBox = ( 21 | draggable: DraggableDimension, 22 | afterCritical: LiftEffect, 23 | ): Spacing => { 24 | // If an item started displaced it is now resting 25 | // in a non-displaced location 26 | const original: Rect = draggable.page.borderBox; 27 | 28 | return didStartAfterCritical(draggable.descriptor.id, afterCritical) 29 | ? offsetByPosition(original, negate(afterCritical.displacedBy.point)) 30 | : original; 31 | }; 32 | -------------------------------------------------------------------------------- /src/state/move-in-direction/move-in-direction-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { DragImpact } from '../../types'; 4 | 5 | export type PublicResult = {| 6 | clientSelection: Position, 7 | impact: DragImpact, 8 | scrollJumpRequest: ?Position, 9 | |}; 10 | -------------------------------------------------------------------------------- /src/state/move-in-direction/move-to-next-place/move-to-next-index/from-reorder.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DraggableDimension, DraggableLocation } from '../../../../types'; 3 | 4 | type Args = {| 5 | isMovingForward: boolean, 6 | isInHomeList: boolean, 7 | location: DraggableLocation, 8 | insideDestination: DraggableDimension[], 9 | |}; 10 | 11 | export default ({ 12 | isMovingForward, 13 | isInHomeList, 14 | insideDestination, 15 | location, 16 | }: Args): ?number => { 17 | // cannot move in the list 18 | if (!insideDestination.length) { 19 | return null; 20 | } 21 | 22 | const currentIndex: number = location.index; 23 | const proposedIndex: number = isMovingForward 24 | ? currentIndex + 1 25 | : currentIndex - 1; 26 | 27 | // Accounting for lists that might not start with an index of 0 28 | const firstIndex: number = insideDestination[0].descriptor.index; 29 | const lastIndex: number = 30 | insideDestination[insideDestination.length - 1].descriptor.index; 31 | 32 | // When in foreign list we allow movement after the last item 33 | const upperBound: number = isInHomeList ? lastIndex : lastIndex + 1; 34 | 35 | if (proposedIndex < firstIndex) { 36 | return null; 37 | } 38 | if (proposedIndex > upperBound) { 39 | return null; 40 | } 41 | 42 | return proposedIndex; 43 | }; 44 | -------------------------------------------------------------------------------- /src/state/no-impact.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | DisplacementGroups, 4 | DragImpact, 5 | DisplacedBy, 6 | LiftEffect, 7 | } from '../types'; 8 | import { origin } from './position'; 9 | 10 | export const noDisplacedBy: DisplacedBy = { 11 | point: origin, 12 | value: 0, 13 | }; 14 | 15 | export const emptyGroups: DisplacementGroups = { 16 | invisible: {}, 17 | visible: {}, 18 | all: [], 19 | }; 20 | 21 | const noImpact: DragImpact = { 22 | displaced: emptyGroups, 23 | displacedBy: noDisplacedBy, 24 | at: null, 25 | }; 26 | 27 | export default noImpact; 28 | 29 | export const noAfterCritical: LiftEffect = { 30 | inVirtualList: false, 31 | effected: {}, 32 | displacedBy: noDisplacedBy, 33 | }; 34 | -------------------------------------------------------------------------------- /src/state/patch-dimension-map.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DimensionMap, DroppableDimension } from '../types'; 3 | import patchDroppableMap from './patch-droppable-map'; 4 | 5 | export default ( 6 | dimensions: DimensionMap, 7 | updated: DroppableDimension, 8 | ): DimensionMap => ({ 9 | draggables: dimensions.draggables, 10 | droppables: patchDroppableMap(dimensions.droppables, updated), 11 | }); 12 | -------------------------------------------------------------------------------- /src/state/patch-droppable-map.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DroppableDimension, DroppableDimensionMap } from '../types'; 3 | 4 | export default ( 5 | droppables: DroppableDimensionMap, 6 | updated: DroppableDimension, 7 | ): DroppableDimensionMap => ({ 8 | ...droppables, 9 | [updated.descriptor.id]: updated, 10 | }); 11 | -------------------------------------------------------------------------------- /src/state/publish-while-dragging-in-virtual/offset-draggable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | withScroll, 4 | offset as offsetBox, 5 | type Position, 6 | type BoxModel, 7 | } from 'css-box-model'; 8 | import type { DraggableDimension } from '../../types'; 9 | 10 | type Args = {| 11 | draggable: DraggableDimension, 12 | offset: Position, 13 | initialWindowScroll: Position, 14 | |}; 15 | 16 | export default ({ 17 | draggable, 18 | offset, 19 | initialWindowScroll, 20 | }: Args): DraggableDimension => { 21 | const client: BoxModel = offsetBox(draggable.client, offset); 22 | const page: BoxModel = withScroll(client, initialWindowScroll); 23 | 24 | const moved: DraggableDimension = { 25 | ...draggable, 26 | placeholder: { 27 | ...draggable.placeholder, 28 | client, 29 | }, 30 | client, 31 | page, 32 | }; 33 | 34 | return moved; 35 | }; 36 | -------------------------------------------------------------------------------- /src/state/rect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getRect } from 'css-box-model'; 3 | import type { Rect, Position } from 'css-box-model'; 4 | import { offsetByPosition } from './spacing'; 5 | 6 | export const offsetRectByPosition = (rect: Rect, point: Position): Rect => 7 | getRect(offsetByPosition(rect, point)); 8 | -------------------------------------------------------------------------------- /src/state/registry/use-registry.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { useEffect } from 'react'; 3 | import { useMemo } from 'use-memo-one'; 4 | import type { Registry } from './registry-types'; 5 | import createRegistry from './create-registry'; 6 | 7 | export default function useRegistry(): Registry { 8 | const registry: Registry = useMemo(createRegistry, []); 9 | 10 | useEffect(() => { 11 | return function unmount() { 12 | // clean up the registry to avoid any leaks 13 | // doing it after an animation frame so that other things unmounting 14 | // can continue to interact with the registry 15 | requestAnimationFrame(registry.clean); 16 | }; 17 | }, [registry]); 18 | 19 | return registry; 20 | } 21 | -------------------------------------------------------------------------------- /src/state/remove-draggable-from-list.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import memoizeOne from 'memoize-one'; 3 | import type { DraggableDimension } from '../types'; 4 | 5 | export default memoizeOne( 6 | ( 7 | remove: DraggableDimension, 8 | list: DraggableDimension[], 9 | ): DraggableDimension[] => 10 | list.filter( 11 | (item: DraggableDimension) => item.descriptor.id !== remove.descriptor.id, 12 | ), 13 | ); 14 | -------------------------------------------------------------------------------- /src/state/scroll-viewport.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getRect, type Rect } from 'css-box-model'; 3 | import type { Position } from 'css-box-model'; 4 | import { subtract, negate } from './position'; 5 | import type { Viewport } from '../types'; 6 | 7 | export default (viewport: Viewport, newScroll: Position): Viewport => { 8 | const diff: Position = subtract(newScroll, viewport.scroll.initial); 9 | const displacement: Position = negate(diff); 10 | 11 | // We need to update the frame so that it is always a live value 12 | // The top / left of the frame should always match the newScroll position 13 | const frame: Rect = getRect({ 14 | top: newScroll.y, 15 | bottom: newScroll.y + viewport.frame.height, 16 | left: newScroll.x, 17 | right: newScroll.x + viewport.frame.width, 18 | }); 19 | 20 | const updated: Viewport = { 21 | frame, 22 | scroll: { 23 | initial: viewport.scroll.initial, 24 | max: viewport.scroll.max, 25 | current: newScroll, 26 | diff: { 27 | value: diff, 28 | displacement, 29 | }, 30 | }, 31 | }; 32 | 33 | return updated; 34 | }; 35 | -------------------------------------------------------------------------------- /src/state/spacing.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Spacing, Position } from 'css-box-model'; 3 | 4 | // TODO add test 5 | export const isEqual = (first: Spacing, second: Spacing): boolean => 6 | first.top === second.top && 7 | first.right === second.right && 8 | first.bottom === second.bottom && 9 | first.left === second.left; 10 | 11 | export const offsetByPosition = ( 12 | spacing: Spacing, 13 | point: Position, 14 | ): Spacing => ({ 15 | top: spacing.top + point.y, 16 | left: spacing.left + point.x, 17 | bottom: spacing.bottom + point.y, 18 | right: spacing.right + point.x, 19 | }); 20 | 21 | export const expandByPosition = ( 22 | spacing: Spacing, 23 | position: Position, 24 | ): Spacing => ({ 25 | // pulling back to increase size 26 | top: spacing.top - position.y, 27 | left: spacing.left - position.x, 28 | // pushing forward to increase size 29 | right: spacing.right + position.x, 30 | bottom: spacing.bottom + position.y, 31 | }); 32 | 33 | export const getCorners = (spacing: Spacing): Position[] => [ 34 | { x: spacing.left, y: spacing.top }, 35 | { x: spacing.right, y: spacing.top }, 36 | { x: spacing.left, y: spacing.bottom }, 37 | { x: spacing.right, y: spacing.bottom }, 38 | ]; 39 | 40 | export const noSpacing: Spacing = { 41 | top: 0, 42 | right: 0, 43 | bottom: 0, 44 | left: 0, 45 | }; 46 | -------------------------------------------------------------------------------- /src/state/store-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // This file exists to avoid a circular dependency between types.js and action-creators.js 3 | import type { 4 | Store as ReduxStore, 5 | Dispatch as ReduxDispatch, 6 | Middleware as ReduxMiddleware, 7 | MiddlewareAPI as ReduxMiddlewareAPI, 8 | } from 'redux'; 9 | import type { Action as ActionCreators } from './action-creators'; 10 | import type { State } from '../types'; 11 | 12 | export type Action = ActionCreators; 13 | export type Dispatch = ReduxDispatch; 14 | export type Store = ReduxStore; 15 | export type Middleware = ReduxMiddleware; 16 | export type MiddlewareStore = ReduxMiddlewareAPI; 17 | -------------------------------------------------------------------------------- /src/state/visibility/is-position-in-frame.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Position, type Spacing } from 'css-box-model'; 3 | import isWithin from '../is-within'; 4 | 5 | export default function isPositionInFrame(frame: Spacing) { 6 | const isWithinVertical = isWithin(frame.top, frame.bottom); 7 | const isWithinHorizontal = isWithin(frame.left, frame.right); 8 | 9 | return function run(point: Position) { 10 | return isWithinVertical(point.y) && isWithinHorizontal(point.x); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/state/visibility/is-totally-visible-through-frame-on-axis.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Spacing } from 'css-box-model'; 3 | import type { Axis } from '../../types'; 4 | import isWithin from '../is-within'; 5 | import { vertical } from '../axis'; 6 | 7 | export default (axis: Axis) => (frame: Spacing) => { 8 | const isWithinVertical = isWithin(frame.top, frame.bottom); 9 | const isWithinHorizontal = isWithin(frame.left, frame.right); 10 | 11 | return (subject: Spacing) => { 12 | if (axis === vertical) { 13 | return isWithinVertical(subject.top) && isWithinVertical(subject.bottom); 14 | } 15 | return ( 16 | isWithinHorizontal(subject.left) && isWithinHorizontal(subject.right) 17 | ); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/state/visibility/is-totally-visible-through-frame.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Spacing } from 'css-box-model'; 3 | import isWithin from '../is-within'; 4 | 5 | export default (frame: Spacing) => { 6 | const isWithinVertical = isWithin(frame.top, frame.bottom); 7 | const isWithinHorizontal = isWithin(frame.left, frame.right); 8 | 9 | return (subject: Spacing) => { 10 | const isContained: boolean = 11 | isWithinVertical(subject.top) && 12 | isWithinVertical(subject.bottom) && 13 | isWithinHorizontal(subject.left) && 14 | isWithinHorizontal(subject.right); 15 | 16 | return isContained; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/state/with-scroll-change/with-all-displacement.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { DroppableDimension, Viewport } from '../../types'; 4 | import withDroppableDisplacement from './with-droppable-displacement'; 5 | import withViewportDisplacement from './with-viewport-displacement'; 6 | 7 | export default ( 8 | page: Position, 9 | droppable: DroppableDimension, 10 | viewport: Viewport, 11 | ): Position => 12 | withDroppableDisplacement( 13 | droppable, 14 | withViewportDisplacement(viewport, page), 15 | ); 16 | -------------------------------------------------------------------------------- /src/state/with-scroll-change/with-droppable-displacement.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Position } from 'css-box-model'; 3 | import { add } from '../position'; 4 | import type { Scrollable, DroppableDimension } from '../../types'; 5 | 6 | export default (droppable: DroppableDimension, point: Position): Position => { 7 | const frame: ?Scrollable = droppable.frame; 8 | if (!frame) { 9 | return point; 10 | } 11 | 12 | return add(point, frame.scroll.diff.displacement); 13 | }; 14 | -------------------------------------------------------------------------------- /src/state/with-scroll-change/with-droppable-scroll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Rect } from 'css-box-model'; 3 | import type { Scrollable, DroppableDimension } from '../../types'; 4 | import { offsetRectByPosition } from '../rect'; 5 | 6 | export default (droppable: DroppableDimension, area: Rect): Rect => { 7 | const frame: ?Scrollable = droppable.frame; 8 | if (!frame) { 9 | return area; 10 | } 11 | 12 | return offsetRectByPosition(area, frame.scroll.diff.value); 13 | }; 14 | -------------------------------------------------------------------------------- /src/state/with-scroll-change/with-viewport-displacement.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Position } from 'css-box-model'; 3 | import type { Viewport } from '../../types'; 4 | import { add } from '../position'; 5 | 6 | export default (viewport: Viewport, point: Position): Position => 7 | add(viewport.scroll.diff.displacement, point); 8 | -------------------------------------------------------------------------------- /src/view/animate-in-out/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './animate-in-out'; 3 | -------------------------------------------------------------------------------- /src/view/check-is-valid-inner-ref.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { invariant } from '../invariant'; 3 | import isHtmlElement from './is-type-of-element/is-html-element'; 4 | 5 | export default function checkIsValidInnerRef(el: ?HTMLElement) { 6 | invariant( 7 | el && isHtmlElement(el), 8 | ` 9 | provided.innerRef has not been provided with a HTMLElement. 10 | 11 | You can find a guide on using the innerRef callback functions at: 12 | https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/using-inner-ref.md 13 | `, 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/view/context/app-context.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import type { DraggableId, ContextId, ElementId } from '../../types'; 4 | import type { DimensionMarshal } from '../../state/dimension-marshal/dimension-marshal-types'; 5 | import type { FocusMarshal } from '../use-focus-marshal/focus-marshal-types'; 6 | import type { Registry } from '../../state/registry/registry-types'; 7 | 8 | export type AppContextValue = {| 9 | focus: FocusMarshal, 10 | contextId: ContextId, 11 | canLift: (id: DraggableId) => boolean, 12 | isMovementAllowed: () => boolean, 13 | dragHandleUsageInstructionsId: ElementId, 14 | marshal: DimensionMarshal, 15 | registry: Registry, 16 | |}; 17 | 18 | export default React.createContext(null); 19 | -------------------------------------------------------------------------------- /src/view/context/droppable-context.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import type { DraggableId, DroppableId, TypeId } from '../../types'; 4 | 5 | export type DroppableContextValue = {| 6 | isUsingCloneFor: ?DraggableId, 7 | droppableId: DroppableId, 8 | type: TypeId, 9 | |}; 10 | 11 | export default React.createContext(null); 12 | -------------------------------------------------------------------------------- /src/view/context/store-context.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import type { Store } from '../../state/store-types'; 4 | 5 | export default React.createContext(null); 6 | -------------------------------------------------------------------------------- /src/view/data-attributes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const prefix: string = 'data-rbd'; 3 | export const dragHandle = (() => { 4 | const base = `${prefix}-drag-handle`; 5 | 6 | return { 7 | base, 8 | draggableId: `${base}-draggable-id`, 9 | contextId: `${base}-context-id`, 10 | }; 11 | })(); 12 | 13 | export const draggable = (() => { 14 | const base: string = `${prefix}-draggable`; 15 | return { 16 | base, 17 | contextId: `${base}-context-id`, 18 | id: `${base}-id`, 19 | }; 20 | })(); 21 | 22 | export const droppable = (() => { 23 | const base: string = `${prefix}-droppable`; 24 | return { 25 | base, 26 | contextId: `${base}-context-id`, 27 | id: `${base}-id`, 28 | }; 29 | })(); 30 | 31 | export const placeholder = { 32 | contextId: `${prefix}-placeholder-context-id`, 33 | }; 34 | 35 | export const scrollContainer = { 36 | contextId: `${prefix}-scroll-container-context-id`, 37 | }; 38 | -------------------------------------------------------------------------------- /src/view/drag-drop-context/check-doctype.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { warning } from '../../dev-warning'; 3 | 4 | const suffix: string = ` 5 | We expect a html5 doctype: 6 | This is to ensure consistent browser layout and measurement 7 | 8 | More information: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/doctype.md 9 | `; 10 | 11 | export default (doc: Document) => { 12 | const doctype: ?DocumentType = doc.doctype; 13 | 14 | if (!doctype) { 15 | warning(` 16 | No found. 17 | 18 | ${suffix} 19 | `); 20 | return; 21 | } 22 | 23 | if (doctype.name.toLowerCase() !== 'html') { 24 | warning(` 25 | Unexpected found: (${doctype.name}) 26 | 27 | ${suffix} 28 | `); 29 | } 30 | 31 | if (doctype.publicId !== '') { 32 | warning(` 33 | Unexpected publicId found: (${doctype.publicId}) 34 | A html5 doctype does not have a publicId 35 | 36 | ${suffix} 37 | `); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/view/drag-drop-context/drag-drop-context-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export type AppCallbacks = {| 3 | isDragging: () => boolean, 4 | tryAbort: () => void, 5 | |}; 6 | 7 | export type SetAppCallbacks = (callbacks: AppCallbacks) => void; 8 | -------------------------------------------------------------------------------- /src/view/drag-drop-context/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default, resetServerContext } from './drag-drop-context'; 3 | -------------------------------------------------------------------------------- /src/view/drag-drop-context/use-startup-validation.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { peerDependencies } from '../../../package.json'; 4 | import checkReactVersion from './check-react-version'; 5 | import checkDoctype from './check-doctype'; 6 | import useDevSetupWarning from '../use-dev-setup-warning'; 7 | 8 | export default function useStartupValidation() { 9 | useDevSetupWarning(() => { 10 | checkReactVersion(peerDependencies.react, React.version); 11 | checkDoctype(document); 12 | }, []); 13 | } 14 | -------------------------------------------------------------------------------- /src/view/drag-drop-context/use-unique-context-id.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { useMemo } from 'use-memo-one'; 3 | import type { ContextId } from '../../types'; 4 | 5 | let count = 0; 6 | 7 | export function reset() { 8 | count = 0; 9 | } 10 | 11 | export default function useInstanceCount(): ContextId { 12 | return useMemo(() => `${count++}`, []); 13 | } 14 | -------------------------------------------------------------------------------- /src/view/draggable/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { PublicDraggable as default } from './draggable-api'; 3 | -------------------------------------------------------------------------------- /src/view/droppable/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './connected-droppable'; 3 | -------------------------------------------------------------------------------- /src/view/event-bindings/bind-events.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { EventBinding, EventOptions } from './event-types'; 3 | 4 | type UnbindFn = () => void; 5 | 6 | function getOptions( 7 | shared?: EventOptions, 8 | fromBinding: ?EventOptions, 9 | ): EventOptions { 10 | return { 11 | ...shared, 12 | ...fromBinding, 13 | }; 14 | } 15 | 16 | export default function bindEvents( 17 | el: HTMLElement, 18 | bindings: EventBinding[], 19 | sharedOptions?: EventOptions, 20 | ): Function { 21 | const unbindings: UnbindFn[] = bindings.map( 22 | (binding: EventBinding): UnbindFn => { 23 | const options: Object = getOptions(sharedOptions, binding.options); 24 | 25 | el.addEventListener(binding.eventName, binding.fn, options); 26 | 27 | return function unbind() { 28 | el.removeEventListener(binding.eventName, binding.fn, options); 29 | }; 30 | }, 31 | ); 32 | 33 | // Return a function to unbind events 34 | return function unbindAll() { 35 | unbindings.forEach((unbind: UnbindFn) => { 36 | unbind(); 37 | }); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/view/event-bindings/event-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type EventOptions = {| 4 | passive?: boolean, 5 | capture?: boolean, 6 | // sometimes an event might only event want to be bound once 7 | once?: boolean, 8 | |}; 9 | 10 | export type EventBinding = {| 11 | eventName: string, 12 | fn: Function, 13 | options?: EventOptions, 14 | |}; 15 | -------------------------------------------------------------------------------- /src/view/get-body-element.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { invariant } from '../invariant'; 3 | 4 | export default (): HTMLBodyElement => { 5 | const body: ?HTMLBodyElement = document.body; 6 | invariant(body, 'Cannot find document.body'); 7 | return body; 8 | }; 9 | -------------------------------------------------------------------------------- /src/view/get-border-box-center-position.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getRect, type Position } from 'css-box-model'; 3 | 4 | export default (el: HTMLElement): Position => 5 | getRect(el.getBoundingClientRect()).center; 6 | -------------------------------------------------------------------------------- /src/view/get-document-element.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { invariant } from '../invariant'; 3 | 4 | export default (): HTMLElement => { 5 | const doc: ?HTMLElement = document.documentElement; 6 | invariant(doc, 'Cannot find document.documentElement'); 7 | return doc; 8 | }; 9 | -------------------------------------------------------------------------------- /src/view/get-elements/find-draggable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DraggableId, ContextId } from '../../types'; 3 | import * as attributes from '../data-attributes'; 4 | import { find, toArray } from '../../native-with-fallback'; 5 | import { warning } from '../../dev-warning'; 6 | import isHtmlElement from '../is-type-of-element/is-html-element'; 7 | 8 | export default function findDraggable( 9 | contextId: ContextId, 10 | draggableId: DraggableId, 11 | ): ?HTMLElement { 12 | // cannot create a selector with the draggable id as it might not be a valid attribute selector 13 | const selector: string = `[${attributes.draggable.contextId}="${contextId}"]`; 14 | const possible: Element[] = toArray(document.querySelectorAll(selector)); 15 | 16 | const draggable: ?Element = find(possible, (el: Element): boolean => { 17 | return el.getAttribute(attributes.draggable.id) === draggableId; 18 | }); 19 | 20 | if (!draggable) { 21 | return null; 22 | } 23 | 24 | if (!isHtmlElement(draggable)) { 25 | warning('Draggable element is not a HTMLElement'); 26 | return null; 27 | } 28 | 29 | return draggable; 30 | } 31 | -------------------------------------------------------------------------------- /src/view/is-strict-equal.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default (a: mixed, b: mixed): boolean => a === b; 3 | -------------------------------------------------------------------------------- /src/view/is-type-of-element/is-element.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getWindowFromEl from '../window/get-window-from-el'; 3 | 4 | export default function isElement(el: Object): boolean %checks { 5 | return el instanceof getWindowFromEl(el).Element; 6 | } 7 | -------------------------------------------------------------------------------- /src/view/is-type-of-element/is-html-element.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getWindowFromEl from '../window/get-window-from-el'; 3 | 4 | export default function isHtmlElement(el: Object): boolean %checks { 5 | return el instanceof getWindowFromEl(el).HTMLElement; 6 | } 7 | -------------------------------------------------------------------------------- /src/view/is-type-of-element/is-svg-element.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getWindowFromEl from '../window/get-window-from-el'; 3 | 4 | export default function isSvgElement(el: Object): boolean %checks { 5 | // Some environments do not support SVGElement 6 | // Doing a double lookup rather than storing the window 7 | // as a %checks function can only be a 'simple predicate' 8 | return ( 9 | Boolean(getWindowFromEl(el).SVGElement) && 10 | el instanceof getWindowFromEl(el).SVGElement 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/view/key-codes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const tab: number = 9; 3 | export const enter: number = 13; 4 | export const escape: number = 27; 5 | export const space: number = 32; 6 | export const pageUp: number = 33; 7 | export const pageDown: number = 34; 8 | export const end: number = 35; 9 | export const home: number = 36; 10 | export const arrowLeft: number = 37; 11 | export const arrowUp: number = 38; 12 | export const arrowRight: number = 39; 13 | export const arrowDown: number = 40; 14 | -------------------------------------------------------------------------------- /src/view/placeholder/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './placeholder'; 3 | -------------------------------------------------------------------------------- /src/view/placeholder/placeholder-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type PlaceholderStyle = {| 4 | display: string, 5 | boxSizing: 'border-box', 6 | width: number, 7 | height: number, 8 | marginTop: number, 9 | marginRight: number, 10 | marginBottom: number, 11 | marginLeft: number, 12 | flexShrink: '0', 13 | flexGrow: '0', 14 | pointerEvents: 'none', 15 | transition: string, 16 | |}; 17 | -------------------------------------------------------------------------------- /src/view/throw-if-invalid-inner-ref.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { invariant } from '../invariant'; 3 | import isHtmlElement from './is-type-of-element/is-html-element'; 4 | 5 | export default (ref: ?mixed) => { 6 | invariant( 7 | ref && isHtmlElement(ref), 8 | ` 9 | provided.innerRef has not been provided with a HTMLElement. 10 | 11 | You can find a guide on using the innerRef callback functions at: 12 | https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/using-inner-ref.md 13 | `, 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/view/use-announcer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './use-announcer'; 3 | -------------------------------------------------------------------------------- /src/view/use-dev-setup-warning.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { useEffect } from 'react'; 3 | import { error } from '../dev-warning'; 4 | import useDev from './use-dev'; 5 | 6 | export default function useDevSetupWarning(fn: () => void, inputs?: mixed[]) { 7 | useDev(() => { 8 | // eslint-disable-next-line react-hooks/rules-of-hooks 9 | useEffect(() => { 10 | try { 11 | fn(); 12 | } catch (e) { 13 | error(` 14 | A setup problem was encountered. 15 | 16 | > ${e.message} 17 | `); 18 | } 19 | // eslint-disable-next-line react-hooks/exhaustive-deps 20 | }, inputs); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/view/use-dev.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default function useDev(useHook: () => void) { 4 | // Don't run any validation in production 5 | if (process.env.NODE_ENV !== 'production') { 6 | // eslint-disable-next-line react-hooks/rules-of-hooks 7 | useHook(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/view/use-draggable-publisher/get-dimension.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | type BoxModel, 4 | type Position, 5 | calculateBox, 6 | withScroll, 7 | } from 'css-box-model'; 8 | import type { 9 | DraggableDescriptor, 10 | DraggableDimension, 11 | Placeholder, 12 | } from '../../types'; 13 | import { origin } from '../../state/position'; 14 | 15 | export default function getDimension( 16 | descriptor: DraggableDescriptor, 17 | el: HTMLElement, 18 | windowScroll?: Position = origin, 19 | ): DraggableDimension { 20 | const computedStyles: CSSStyleDeclaration = window.getComputedStyle(el); 21 | const borderBox: ClientRect = el.getBoundingClientRect(); 22 | const client: BoxModel = calculateBox(borderBox, computedStyles); 23 | const page: BoxModel = withScroll(client, windowScroll); 24 | 25 | const placeholder: Placeholder = { 26 | client, 27 | tagName: el.tagName.toLowerCase(), 28 | display: computedStyles.display, 29 | }; 30 | const displaceBy: Position = { 31 | x: client.marginBox.width, 32 | y: client.marginBox.height, 33 | }; 34 | 35 | const dimension: DraggableDimension = { 36 | descriptor, 37 | placeholder, 38 | displaceBy, 39 | client, 40 | page, 41 | }; 42 | 43 | return dimension; 44 | } 45 | -------------------------------------------------------------------------------- /src/view/use-draggable-publisher/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './use-draggable-publisher'; 3 | -------------------------------------------------------------------------------- /src/view/use-droppable-publisher/check-for-nested-scroll-container.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getClosestScrollable from './get-closest-scrollable'; 3 | import { warning } from '../../dev-warning'; 4 | 5 | // We currently do not support nested scroll containers 6 | // But will hopefully support this soon! 7 | export default (scrollable: ?Element) => { 8 | if (!scrollable) { 9 | return; 10 | } 11 | 12 | const anotherScrollParent: ?Element = getClosestScrollable( 13 | scrollable.parentElement, 14 | ); 15 | 16 | if (!anotherScrollParent) { 17 | return; 18 | } 19 | 20 | warning(` 21 | Droppable: unsupported nested scroll container detected. 22 | A Droppable can only have one scroll parent (which can be itself) 23 | Nested scroll containers are currently not supported. 24 | 25 | We hope to support nested scroll containers soon: https://github.com/atlassian/react-beautiful-dnd/issues/131 26 | `); 27 | }; 28 | -------------------------------------------------------------------------------- /src/view/use-droppable-publisher/get-env.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getClosestScrollable from './get-closest-scrollable'; 3 | 4 | export type Env = {| 5 | closestScrollable: ?Element, 6 | isFixedOnPage: boolean, 7 | |}; 8 | 9 | // TODO: do this check at the same time as the closest scrollable 10 | // in order to avoid double calling getComputedStyle 11 | // Do this when we move to multiple scroll containers 12 | const getIsFixed = (el: ?Element): boolean => { 13 | if (!el) { 14 | return false; 15 | } 16 | const style: CSSStyleDeclaration = window.getComputedStyle(el); 17 | if (style.position === 'fixed') { 18 | return true; 19 | } 20 | return getIsFixed(el.parentElement); 21 | }; 22 | 23 | export default (start: Element): Env => { 24 | const closestScrollable: ?Element = getClosestScrollable(start); 25 | const isFixedOnPage: boolean = getIsFixed(start); 26 | 27 | return { 28 | closestScrollable, 29 | isFixedOnPage, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/view/use-droppable-publisher/get-listener-options.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ScrollOptions } from '../../types'; 3 | 4 | const immediate = { 5 | passive: false, 6 | }; 7 | const delayed = { 8 | passive: true, 9 | }; 10 | 11 | export default (options: ScrollOptions) => 12 | options.shouldPublishImmediately ? immediate : delayed; 13 | -------------------------------------------------------------------------------- /src/view/use-droppable-publisher/get-scroll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | 4 | export default (el: Element): Position => ({ 5 | x: el.scrollLeft, 6 | y: el.scrollTop, 7 | }); 8 | -------------------------------------------------------------------------------- /src/view/use-droppable-publisher/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './use-droppable-publisher'; 3 | -------------------------------------------------------------------------------- /src/view/use-droppable-publisher/is-in-fixed-container.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const isElementFixed = (el: Element): boolean => 4 | window.getComputedStyle(el).position === 'fixed'; 5 | 6 | const find = (el: ?Element): boolean => { 7 | // cannot do anything else! 8 | if (el == null) { 9 | return false; 10 | } 11 | 12 | // keep looking 13 | if (!isElementFixed(el)) { 14 | return find(el.parentElement); 15 | } 16 | 17 | // success! 18 | return true; 19 | }; 20 | 21 | export default find; 22 | -------------------------------------------------------------------------------- /src/view/use-focus-marshal/focus-marshal-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DraggableId } from '../../types'; 3 | 4 | export type Unregister = () => void; 5 | 6 | export type Register = (id: DraggableId, focus: () => void) => Unregister; 7 | 8 | export type FocusMarshal = {| 9 | register: Register, 10 | tryRecordFocus: (tryRecordFor: DraggableId) => void, 11 | tryRestoreFocusRecorded: () => void, 12 | tryShiftRecord: (previous: DraggableId, redirectTo: DraggableId) => void, 13 | |}; 14 | -------------------------------------------------------------------------------- /src/view/use-focus-marshal/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './use-focus-marshal'; 3 | -------------------------------------------------------------------------------- /src/view/use-hidden-text-element/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './use-hidden-text-element'; 3 | -------------------------------------------------------------------------------- /src/view/use-isomorphic-layout-effect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // eslint-disable-next-line no-restricted-imports 3 | import { useLayoutEffect, useEffect } from 'react'; 4 | 5 | // https://github.com/reduxjs/react-redux/blob/v7-beta/src/components/connectAdvanced.js#L35 6 | // React currently throws a warning when using useLayoutEffect on the server. 7 | // To get around it, we can conditionally useEffect on the server (no-op) and 8 | // useLayoutEffect in the browser. We need useLayoutEffect because we want 9 | // `connect` to perform sync updates to a ref to save the latest props after 10 | // a render is actually committed to the DOM. 11 | const useIsomorphicLayoutEffect = 12 | typeof window !== 'undefined' && 13 | typeof window.document !== 'undefined' && 14 | typeof window.document.createElement !== 'undefined' 15 | ? useLayoutEffect 16 | : useEffect; 17 | 18 | export default useIsomorphicLayoutEffect; 19 | -------------------------------------------------------------------------------- /src/view/use-previous-ref.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { useRef, useEffect } from 'react'; 3 | 4 | export default function usePrevious(current: T): {| current: T |} { 5 | const ref = useRef(current); 6 | 7 | // will be updated on the next render 8 | useEffect(() => { 9 | ref.current = current; 10 | }); 11 | 12 | // return the existing current (pre render) 13 | return ref; 14 | } 15 | -------------------------------------------------------------------------------- /src/view/use-required-context.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { useContext, type Context as ContextType } from 'react'; 3 | import { invariant } from '../invariant'; 4 | 5 | export default function useRequiredContext(Context: ContextType): T { 6 | const result: ?T = useContext(Context); 7 | invariant(result, 'Could not find required context'); 8 | return result; 9 | } 10 | -------------------------------------------------------------------------------- /src/view/use-sensor-marshal/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './use-sensor-marshal'; 3 | export { default as useMouseSensor } from './sensors/use-mouse-sensor'; 4 | export { default as useTouchSensor } from './sensors/use-touch-sensor'; 5 | export { default as useKeyboardSensor } from './sensors/use-keyboard-sensor'; 6 | -------------------------------------------------------------------------------- /src/view/use-sensor-marshal/lock.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { invariant } from '../../invariant'; 3 | 4 | export type Lock = {| 5 | abandon: () => void, 6 | |}; 7 | 8 | export type LockAPI = {| 9 | isClaimed: () => boolean, 10 | isActive: (lock: Lock) => boolean, 11 | claim: (abandon: () => void) => Lock, 12 | release: () => void, 13 | tryAbandon: () => void, 14 | |}; 15 | 16 | export default function create(): LockAPI { 17 | let lock: ?Lock = null; 18 | 19 | function isClaimed(): boolean { 20 | return Boolean(lock); 21 | } 22 | 23 | function isActive(value: Lock): boolean { 24 | return value === lock; 25 | } 26 | 27 | function claim(abandon: () => void): Lock { 28 | invariant(!lock, 'Cannot claim lock as it is already claimed'); 29 | const newLock: Lock = { abandon }; 30 | // update singleton 31 | lock = newLock; 32 | // return lock 33 | return newLock; 34 | } 35 | function release() { 36 | invariant(lock, 'Cannot release lock when there is no lock'); 37 | lock = null; 38 | } 39 | 40 | function tryAbandon() { 41 | if (lock) { 42 | lock.abandon(); 43 | release(); 44 | } 45 | } 46 | 47 | return { isClaimed, isActive, claim, release, tryAbandon }; 48 | } 49 | -------------------------------------------------------------------------------- /src/view/use-sensor-marshal/sensors/util/prevent-standard-key-events.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as keyCodes from '../../../key-codes'; 3 | 4 | type KeyMap = { 5 | [key: number]: true, 6 | }; 7 | 8 | const preventedKeys: KeyMap = { 9 | // submission 10 | [keyCodes.enter]: true, 11 | // tabbing 12 | [keyCodes.tab]: true, 13 | }; 14 | 15 | export default (event: KeyboardEvent) => { 16 | if (preventedKeys[event.keyCode]) { 17 | event.preventDefault(); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/view/use-sensor-marshal/sensors/util/supported-page-visibility-event-name.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { find } from '../../../../native-with-fallback'; 3 | 4 | const supportedEventName: string = ((): string => { 5 | const base: string = 'visibilitychange'; 6 | 7 | // Server side rendering 8 | if (typeof document === 'undefined') { 9 | return base; 10 | } 11 | 12 | // See https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API 13 | const candidates: string[] = [ 14 | base, 15 | `ms${base}`, 16 | `webkit${base}`, 17 | `moz${base}`, 18 | `o${base}`, 19 | ]; 20 | 21 | const supported: ?string = find( 22 | candidates, 23 | (eventName: string): boolean => `on${eventName}` in document, 24 | ); 25 | 26 | return supported || base; 27 | })(); 28 | 29 | export default supportedEventName; 30 | -------------------------------------------------------------------------------- /src/view/use-sensor-marshal/use-validate-sensor-hooks.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable react-hooks/rules-of-hooks */ 3 | import { invariant } from '../../invariant'; 4 | import type { Sensor } from '../../types'; 5 | import usePreviousRef from '../use-previous-ref'; 6 | import useDevSetupWarning from '../use-dev-setup-warning'; 7 | import useDev from '../use-dev'; 8 | 9 | export default function useValidateSensorHooks(sensorHooks: Sensor[]) { 10 | useDev(() => { 11 | const previousRef = usePreviousRef(sensorHooks); 12 | 13 | useDevSetupWarning(() => { 14 | invariant( 15 | previousRef.current.length === sensorHooks.length, 16 | 'Cannot change the amount of sensor hooks after mounting', 17 | ); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/view/use-style-marshal/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './use-style-marshal'; 3 | -------------------------------------------------------------------------------- /src/view/use-style-marshal/style-marshal-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DropReason } from '../../types'; 3 | 4 | export type StyleMarshal = {| 5 | dragging: () => void, 6 | dropping: (reason: DropReason) => void, 7 | resting: () => void, 8 | |}; 9 | -------------------------------------------------------------------------------- /src/view/use-unique-id.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { useMemo } from 'use-memo-one'; 3 | import type { Id } from '../types'; 4 | 5 | let count: number = 0; 6 | 7 | type Options = { 8 | separator: string, 9 | }; 10 | 11 | const defaults: Options = { separator: '::' }; 12 | 13 | export function reset() { 14 | count = 0; 15 | } 16 | 17 | export default function useUniqueId( 18 | prefix: string, 19 | options?: Options = defaults, 20 | ): Id { 21 | return useMemo(() => `${prefix}${options.separator}${count++}`, [ 22 | options.separator, 23 | prefix, 24 | ]); 25 | } 26 | -------------------------------------------------------------------------------- /src/view/visually-hidden-style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // https://allyjs.io/tutorials/hiding-elements.html 3 | // Element is visually hidden but is readable by screen readers 4 | const visuallyHidden: Object = { 5 | position: 'absolute', 6 | width: '1px', 7 | height: '1px', 8 | margin: '-1px', 9 | border: '0', 10 | padding: '0', 11 | overflow: 'hidden', 12 | clip: 'rect(0 0 0 0)', 13 | 'clip-path': 'inset(100%)', 14 | }; 15 | 16 | export default visuallyHidden; 17 | -------------------------------------------------------------------------------- /src/view/window/get-max-window-scroll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import getMaxScroll from '../../state/get-max-scroll'; 4 | import getDocumentElement from '../get-document-element'; 5 | 6 | export default (): Position => { 7 | const doc: HTMLElement = getDocumentElement(); 8 | 9 | const maxScroll: Position = getMaxScroll({ 10 | // unclipped padding box, with scrollbar 11 | scrollHeight: doc.scrollHeight, 12 | scrollWidth: doc.scrollWidth, 13 | // clipped padding box, without scrollbar 14 | width: doc.clientWidth, 15 | height: doc.clientHeight, 16 | }); 17 | 18 | return maxScroll; 19 | }; 20 | -------------------------------------------------------------------------------- /src/view/window/get-window-from-el.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default (el: ?Element): typeof window => 3 | el && el.ownerDocument ? el.ownerDocument.defaultView : window; 4 | -------------------------------------------------------------------------------- /src/view/window/get-window-scroll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Position } from 'css-box-model'; 3 | 4 | // The browsers update document.documentElement.scrollTop and window.pageYOffset 5 | // differently as the window scrolls. 6 | 7 | // Webkit 8 | // documentElement.scrollTop: no update. Stays at 0 9 | // window.pageYOffset: updates to whole number 10 | 11 | // Chrome 12 | // documentElement.scrollTop: update with fractional value 13 | // window.pageYOffset: update with fractional value 14 | 15 | // FireFox 16 | // documentElement.scrollTop: updates to whole number 17 | // window.pageYOffset: updates to whole number 18 | 19 | // IE11 (same as firefox) 20 | // documentElement.scrollTop: updates to whole number 21 | // window.pageYOffset: updates to whole number 22 | 23 | // Edge (same as webkit) 24 | // documentElement.scrollTop: no update. Stays at 0 25 | // window.pageYOffset: updates to whole number 26 | 27 | export default (): Position => ({ 28 | x: window.pageXOffset, 29 | y: window.pageYOffset, 30 | }); 31 | -------------------------------------------------------------------------------- /src/view/window/scroll-window.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Position } from 'css-box-model'; 3 | 4 | // Not guarenteed to scroll by the entire amount 5 | export default (change: Position): void => { 6 | window.scrollBy(change.x, change.y); 7 | }; 8 | -------------------------------------------------------------------------------- /stories/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // allowing console.warn / console.error 4 | // this is because we often mock console.warn and console.error and adding this rul 5 | // avoids needing to constantly be opting out of the rule 6 | 'no-console': ['error', { allow: ['warn', 'error'] }], 7 | 8 | // allowing useMemo and useCallback 9 | 'no-restricted-imports': 'off', 10 | 11 | // Allowing Array.from 12 | 'no-restricted-syntax': 'off', 13 | 14 | // Sometimes used for simple examples 15 | 'react/state-in-constructor': 'off', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /stories/10-table.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import WithDimensionLocking from './src/table/with-dimension-locking'; 5 | import WithFixedColumns from './src/table/with-fixed-columns'; 6 | import WithPortal from './src/table/with-portal'; 7 | import WithClone from './src/table/with-clone'; 8 | import { quotes } from './src/data'; 9 | 10 | storiesOf('Tables', module) 11 | .add('with fixed width columns', () => ) 12 | .add('with dimension locking', () => ( 13 | 14 | )) 15 | .add('with clone', () => ) 16 | .add('with custom portal', () => ); 17 | -------------------------------------------------------------------------------- /stories/11-portal.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import PortalApp from './src/portal/portal-app'; 5 | import { quotes } from './src/data'; 6 | 7 | storiesOf('Portals', module).add('Using your own portal', () => ( 8 | 9 | )); 10 | -------------------------------------------------------------------------------- /stories/12-dynamic.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import WithControls from './src/dynamic/with-controls'; 5 | import LazyLoading from './src/dynamic/lazy-loading'; 6 | 7 | storiesOf('Dynamic changes during a drag (v11 only)', module) 8 | .add('With controls', () => ) 9 | .add('Lazy loading', () => ); 10 | -------------------------------------------------------------------------------- /stories/15-on-before-capture.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import AddingThings from './src/on-before-capture/adding-things'; 5 | 6 | storiesOf('onBeforeCapture', module).add('adding things', () => ( 7 | 8 | )); 9 | -------------------------------------------------------------------------------- /stories/2-single-horizontal.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import styled from '@emotion/styled'; 5 | import AuthorApp from './src/horizontal/author-app'; 6 | import { quotes, getQuotes } from './src/data'; 7 | import type { Quote } from './src/types'; 8 | 9 | const bigData: Quote[] = getQuotes(30); 10 | 11 | const WideWindow = styled.div` 12 | width: 120vw; 13 | `; 14 | 15 | storiesOf('single horizontal list', module) 16 | .add('simple', () => ) 17 | .add('with combine enabled', () => ( 18 | 19 | )) 20 | .add('with overflow scroll', () => ( 21 | 22 | )) 23 | .add('with window scroll and overflow scroll', () => ( 24 | 25 | 26 | 27 | )); 28 | -------------------------------------------------------------------------------- /stories/20-super-simple.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import Simple from './src/simple/simple'; 5 | import SimpleWithScroll from './src/simple/simple-scrollable'; 6 | import WithMixedSpacing from './src/simple/simple-mixed-spacing'; 7 | 8 | storiesOf('Super simple', module) 9 | .add('vertical list', () => ) 10 | .add('vertical list with scroll (overflow: auto)', () => ( 11 | 12 | )) 13 | .add('vertical list with scroll (overflow: scroll)', () => ( 14 | 15 | )) 16 | .add('with mixed spacing', () => ); 17 | -------------------------------------------------------------------------------- /stories/25-fixed-list.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import WithFixedSidebar from './src/fixed-list/fixed-sidebar'; 5 | 6 | storiesOf('fixed list', module).add('with fixed sidebar', () => ( 7 | 8 | )); 9 | -------------------------------------------------------------------------------- /stories/3-board.stories.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import Board from './src/board/board'; 5 | import { authorQuoteMap, generateQuoteMap } from './src/data'; 6 | 7 | const data = { 8 | medium: generateQuoteMap(100), 9 | large: generateQuoteMap(500), 10 | }; 11 | 12 | storiesOf('board', module) 13 | .add('simple', () => ) 14 | .add('dragging a clone', () => ) 15 | .add('medium data set', () => ) 16 | .add('large data set', () => ) 17 | .add('long lists in a short container', () => ( 18 | 19 | )) 20 | .add('scrollable columns', () => ( 21 | 22 | )) 23 | .add('with combining', () => ( 24 | 25 | )) 26 | .add('with combining and cloning', () => ( 27 | 28 | )); 29 | -------------------------------------------------------------------------------- /stories/30-custom-drop.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import FunnyDrop from './src/custom-drop/funny-drop'; 5 | import NoDrop from './src/custom-drop/no-drop'; 6 | 7 | storiesOf('Custom drop animation', module) 8 | .add('funny drop animation', () => ) 9 | .add('no drop animation', () => ); 10 | -------------------------------------------------------------------------------- /stories/35-function-component.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import QuoteApp from './src/function-component/quote-app'; 5 | 6 | storiesOf( 7 | 'Function component usage', 8 | module, 9 | ).add('using rbd with function components and hooks', () => ); 10 | -------------------------------------------------------------------------------- /stories/4-complex-vertical-list.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import NestedQuoteApp from './src/vertical-nested/quote-app'; 5 | import GroupedQuoteApp from './src/vertical-grouped/quote-app'; 6 | import { authorQuoteMap } from './src/data'; 7 | 8 | storiesOf('complex vertical list', module) 9 | .add('grouped', () => ) 10 | // this is kind of strange - but hey, if you want to! 11 | .add('nested vertical lists', () => ); 12 | -------------------------------------------------------------------------------- /stories/40-programmatic.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import WithControls from './src/programmatic/with-controls'; 5 | import Runsheet from './src/programmatic/runsheet'; 6 | import { quotes } from './src/data'; 7 | 8 | storiesOf('Programmatic dragging', module) 9 | .add('with controls', () => ) 10 | .add('with runsheet', () => ); 11 | -------------------------------------------------------------------------------- /stories/45-virtual.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import ReactWindowList from './src/virtual/react-window/list'; 5 | import ReactVirtualizedList from './src/virtual/react-virtualized/list'; 6 | import { getQuotes } from './src/data'; 7 | import ReactWindowBoard from './src/virtual/react-window/board'; 8 | import ReactVirtualizedBoard from './src/virtual/react-virtualized/board'; 9 | import ReactVirtualizedWindowList from './src/virtual/react-virtualized/window-list'; 10 | 11 | storiesOf('Virtual: react-window', module) 12 | .add('list', () => ) 13 | .add('board', () => ); 14 | 15 | storiesOf('Virtual: react-virtualized', module) 16 | .add('list', () => ) 17 | .add('board', () => ) 18 | .add('window list', () => ( 19 | 20 | )); 21 | -------------------------------------------------------------------------------- /stories/5-multiple-vertical-lists.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import QuoteApp from './src/multiple-vertical/quote-app'; 5 | import { getQuotes } from './src/data'; 6 | import type { QuoteMap } from './src/types'; 7 | 8 | const alpha: string = 'alpha'; 9 | const beta: string = 'beta'; 10 | const gamma: string = 'gamma'; 11 | const delta: string = 'delta'; 12 | const epsilon: string = 'epsilon'; 13 | const zeta: string = 'zeta'; 14 | const eta: string = 'eta'; 15 | const theta: string = 'theta'; 16 | const iota: string = 'iota'; 17 | const kappa: string = 'kappa'; 18 | 19 | const quoteMap: QuoteMap = { 20 | [alpha]: getQuotes(7), 21 | [beta]: getQuotes(3), 22 | [gamma]: getQuotes(7), 23 | [delta]: getQuotes(2), 24 | [epsilon]: getQuotes(10), 25 | [zeta]: getQuotes(5), 26 | [eta]: getQuotes(5), 27 | [theta]: getQuotes(5), 28 | [iota]: getQuotes(20), 29 | [kappa]: getQuotes(5), 30 | }; 31 | 32 | storiesOf('multiple vertical lists', module).add('stress test', () => ( 33 | 34 | )); 35 | -------------------------------------------------------------------------------- /stories/50-multiple-contexts.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import MultipleContexts from './src/programmatic/multiple-contexts'; 5 | 6 | storiesOf('Multiple contexts', module).add('with multiple contexts', () => ( 7 | 8 | )); 9 | -------------------------------------------------------------------------------- /stories/55-mixed-sizes.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import MixedSizedItems from './src/mixed-sizes/mixed-size-items'; 5 | import MixedSizedLists from './src/mixed-sizes/mixed-size-lists'; 6 | import Experiment from './src/mixed-sizes/mixed-size-lists-experiment'; 7 | 8 | storiesOf('mixed sizes', module) 9 | .add('with large draggable size variance', () => ) 10 | .add('with large droppable size variance', () => ) 11 | .add('with large droppable size variance (experiment)', () => ); 12 | -------------------------------------------------------------------------------- /stories/6-multiple-horizontal-lists.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import QuoteApp from './src/multiple-horizontal/quote-app'; 5 | import { getQuotes } from './src/data'; 6 | import type { QuoteMap } from './src/types'; 7 | 8 | const alpha: string = 'alpha'; 9 | const beta: string = 'beta'; 10 | const gamma: string = 'gamma'; 11 | 12 | const quoteMap: QuoteMap = { 13 | [alpha]: getQuotes(20), 14 | [beta]: getQuotes(18), 15 | [gamma]: getQuotes(22), 16 | }; 17 | 18 | storiesOf('multiple horizontal lists', module).add('stress test', () => ( 19 | 20 | )); 21 | -------------------------------------------------------------------------------- /stories/7-interactive-elements.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import InteractiveElementsApp from './src/interactive-elements/interactive-elements-app'; 5 | 6 | storiesOf('nested interative elements', module).add('stress test', () => ( 7 | 8 | )); 9 | -------------------------------------------------------------------------------- /stories/8-accessibility.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import TaskApp from './src/accessible/task-app'; 5 | 6 | storiesOf('Accessibility', module).add('single list', () => ); 7 | -------------------------------------------------------------------------------- /stories/9-multi-drag.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import TaskApp from './src/multi-drag/task-app'; 5 | 6 | storiesOf('Multi drag', module).add('pattern', () => ); 7 | -------------------------------------------------------------------------------- /stories/99-debug.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | // import { Draggable, Droppable, DragDropContext } from '../src'; 5 | 6 | class App extends React.Component<*> { 7 | render() { 8 | return 'Used for debugging codesandbox examples (copy paste them into this file)'; 9 | } 10 | } 11 | 12 | storiesOf('Troubleshoot example', module).add('debug example', () => ); 13 | -------------------------------------------------------------------------------- /stories/src/accessible/blur-context.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | 4 | const BlurContext = React.createContext(0); 5 | 6 | export default BlurContext; 7 | -------------------------------------------------------------------------------- /stories/src/accessible/data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Task } from '../types'; 3 | 4 | const tasks: Task[] = [ 5 | { 6 | id: '1', 7 | content: 'Eat lunch', 8 | }, 9 | { 10 | id: '2', 11 | content: 'Finish that book I have been reading', 12 | }, 13 | { 14 | id: '3', 15 | content: 'Go to the store', 16 | }, 17 | ]; 18 | 19 | export default tasks; 20 | -------------------------------------------------------------------------------- /stories/src/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const grid: number = 8; 4 | export const borderRadius: number = 2; 5 | -------------------------------------------------------------------------------- /stories/src/multi-drag/data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Column, Entities, TaskMap } from './types'; 3 | import type { Task, Id } from '../types'; 4 | 5 | const tasks: Task[] = Array.from({ length: 20 }, (v, k) => k).map( 6 | (val: number): Task => ({ 7 | id: `task-${val}`, 8 | content: `Task ${val}`, 9 | }), 10 | ); 11 | 12 | const taskMap: TaskMap = tasks.reduce( 13 | (previous: TaskMap, current: Task): TaskMap => { 14 | previous[current.id] = current; 15 | return previous; 16 | }, 17 | {}, 18 | ); 19 | 20 | const todo: Column = { 21 | id: 'todo', 22 | title: 'To do', 23 | taskIds: tasks.map((task: Task): Id => task.id), 24 | }; 25 | 26 | const done: Column = { 27 | id: 'done', 28 | title: 'Done', 29 | taskIds: [], 30 | }; 31 | 32 | const entities: Entities = { 33 | columnOrder: [todo.id, done.id], 34 | columns: { 35 | [todo.id]: todo, 36 | [done.id]: done, 37 | }, 38 | tasks: taskMap, 39 | }; 40 | 41 | export default entities; 42 | -------------------------------------------------------------------------------- /stories/src/multi-drag/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Id, Task } from '../types'; 3 | 4 | export type Column = {| 5 | id: Id, 6 | title: string, 7 | taskIds: Id[], 8 | |}; 9 | 10 | export type ColumnMap = { 11 | [columnId: Id]: Column, 12 | }; 13 | 14 | export type TaskMap = { 15 | [taskId: Id]: Task, 16 | }; 17 | 18 | export type Entities = {| 19 | columnOrder: Id[], 20 | columns: ColumnMap, 21 | tasks: TaskMap, 22 | |}; 23 | -------------------------------------------------------------------------------- /stories/src/primatives/title.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from '@emotion/styled'; 3 | import { colors } from '@atlaskit/theme'; 4 | import { grid } from '../constants'; 5 | 6 | // $ExpectError - not sure why 7 | export default styled.h4` 8 | padding: ${grid}px; 9 | transition: background-color ease 0.2s; 10 | flex-grow: 1; 11 | user-select: none; 12 | position: relative; 13 | 14 | &:focus { 15 | outline: 2px solid ${colors.P100}; 16 | outline-offset: 2px; 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /stories/src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DraggableId, DraggableLocation } from '../../src/types'; 3 | 4 | export type Id = string; 5 | 6 | export type AuthorColors = {| 7 | soft: string, 8 | hard: string, 9 | |}; 10 | 11 | export type Author = {| 12 | id: Id, 13 | name: string, 14 | avatarUrl: string, 15 | url: string, 16 | colors: AuthorColors, 17 | |}; 18 | 19 | export type Quote = {| 20 | id: Id, 21 | content: string, 22 | author: Author, 23 | |}; 24 | 25 | export type Dragging = {| 26 | id: DraggableId, 27 | location: DraggableLocation, 28 | |}; 29 | 30 | export type QuoteMap = { 31 | [key: string]: Quote[], 32 | }; 33 | 34 | export type Task = {| 35 | id: Id, 36 | content: string, 37 | |}; 38 | -------------------------------------------------------------------------------- /stories/src/vertical-nested/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type NestedQuoteList = {| 4 | id: string, 5 | title: string, 6 | children: Array, 7 | |}; 8 | -------------------------------------------------------------------------------- /stories/static/media/bmo-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/stories/static/media/bmo-min.png -------------------------------------------------------------------------------- /stories/static/media/bmo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/stories/static/media/bmo.png -------------------------------------------------------------------------------- /stories/static/media/finn-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/stories/static/media/finn-min.png -------------------------------------------------------------------------------- /stories/static/media/finn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/stories/static/media/finn.png -------------------------------------------------------------------------------- /stories/static/media/jake-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/stories/static/media/jake-min.png -------------------------------------------------------------------------------- /stories/static/media/jake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/stories/static/media/jake.png -------------------------------------------------------------------------------- /stories/static/media/princess-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/stories/static/media/princess-min.png -------------------------------------------------------------------------------- /stories/static/media/princess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/stories/static/media/princess.png -------------------------------------------------------------------------------- /test-reports/lighthouse/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/test-reports/lighthouse/.gitkeep -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // allowing console.warn / console.error 4 | // this is because we often mock console.warn and console.error and adding this rul 5 | // avoids needing to constantly be opting out of the rule 6 | 'no-console': ['error', { allow: ['warn', 'error'] }], 7 | 8 | // allowing useMemo and useCallback in tests 9 | 'no-restricted-imports': 'off', 10 | 11 | // Allowing Array.from 12 | 'no-restricted-syntax': 'off', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/test-setup.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // ensuring that each test has at least one assertion 4 | beforeEach(expect.hasAssertions); 5 | 6 | if (typeof document !== 'undefined') { 7 | // Simply importing this package will throw an error if document is not defined 8 | // eslint-disable-next-line global-require 9 | const { cleanup, fireEvent } = require('@testing-library/react'); 10 | 11 | // unmount any components mounted with react-testing-library 12 | beforeAll(cleanup); 13 | afterEach(() => { 14 | cleanup(); 15 | // lots of tests can leave a post-drop click blocker 16 | // this cleans it up before every test 17 | fireEvent.click(window); 18 | 19 | // Cleaning up any mocks 20 | 21 | if (window.getComputedStyle.mockRestore) { 22 | window.getComputedStyle.mockRestore(); 23 | } 24 | 25 | if (Element.prototype.getBoundingClientRect.mockRestore) { 26 | Element.prototype.getBoundingClientRect.mockRestore(); 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/unit/dev-warning.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { warning } from '../../src/dev-warning'; 3 | 4 | const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); 5 | 6 | afterEach(() => { 7 | warn.mockClear(); 8 | }); 9 | 10 | it('should log a warning to the console', () => { 11 | warning('hey'); 12 | 13 | expect(warn).toHaveBeenCalled(); 14 | }); 15 | 16 | it('should not log a warning if warnings are disabled', () => { 17 | window['__react-beautiful-dnd-disable-dev-warnings'] = true; 18 | 19 | warning('hey'); 20 | warning('sup'); 21 | warning('hi'); 22 | 23 | expect(warn).not.toHaveBeenCalled(); 24 | 25 | // re-enable 26 | 27 | window['__react-beautiful-dnd-disable-dev-warnings'] = false; 28 | 29 | warning('hey'); 30 | 31 | expect(warn).toHaveBeenCalled(); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/health/src-file-name-convention.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import globby from 'globby'; 3 | import { invariant } from '../../../src/invariant'; 4 | import pkg from '../../../package.json'; 5 | 6 | // Regex playground: https://regexr.com/40fin 7 | const convention: RegExp = /^[a-z0-9\-./]+$/; 8 | const isSnakeCase = (filePath: string): boolean => convention.test(filePath); 9 | 10 | const exceptions: string[] = [ 11 | 'CHANGELOG.md', 12 | 'CODE_OF_CONDUCT.md', 13 | 'CONTRIBUTING.md', 14 | 'ISSUE_TEMPLATE.md', 15 | 'README.md', 16 | ]; 17 | 18 | it('should have every prettier target following the file name convention', async () => { 19 | const targets: string[] = pkg.config.prettier_target.split(' '); 20 | const paths: string[] = await globby(targets); 21 | 22 | invariant( 23 | paths.length, 24 | 'Could not find files to test against file name convention', 25 | ); 26 | 27 | paths.forEach((filePath: string) => { 28 | if (exceptions.includes(filePath)) { 29 | return; 30 | } 31 | 32 | const isMatching: boolean = isSnakeCase(filePath); 33 | 34 | invariant( 35 | isMatching, 36 | `${filePath} does not follow the file path convention (snake-case.js) ${convention.toString()}`, 37 | ); 38 | 39 | expect(isMatching).toBe(true); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/integration/accessibility/axe-audit.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render, cleanup } from '@testing-library/react'; 4 | import { axe, toHaveNoViolations } from 'jest-axe'; 5 | 6 | import App from '../util/app'; 7 | 8 | expect.extend(toHaveNoViolations); 9 | 10 | // Need to investigate this further as it doesn't seem to pick up the same errors 11 | // which are found by react-axe. 12 | it('should not fail an aXe audit', async () => { 13 | render(); 14 | 15 | const results = await axe(document.body); 16 | 17 | // $FlowFixMe - flow doesn't know about hte custom validator 18 | expect(results).toHaveNoViolations(); 19 | 20 | cleanup(); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/integration/body-removal-before-unmount.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { isDragging } from './util/helpers'; 5 | import App from './util/app'; 6 | import { forEachSensor, simpleLift, type Control } from './util/controls'; 7 | import getBodyElement from '../../../src/view/get-body-element'; 8 | 9 | it('should have any errors when body is changed just before unmount', () => { 10 | jest.useFakeTimers(); 11 | const { unmount } = render(); 12 | 13 | expect(() => { 14 | getBodyElement().innerHTML = ''; 15 | unmount(); 16 | jest.runOnlyPendingTimers(); 17 | }).not.toThrow(); 18 | 19 | jest.useRealTimers(); 20 | }); 21 | 22 | forEachSensor((control: Control) => { 23 | it('should have any errors when body is changed just before unmount: mid drag', () => { 24 | const { unmount, getByText } = render(); 25 | const handle: HTMLElement = getByText('item: 0'); 26 | 27 | // mid drag 28 | simpleLift(control, handle); 29 | expect(isDragging(handle)).toEqual(true); 30 | 31 | expect(() => { 32 | getBodyElement().innerHTML = ''; 33 | unmount(); 34 | jest.runOnlyPendingTimers(); 35 | }).not.toThrow(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/unit/integration/drag-drop-context/check-doctype.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { JSDOM } from 'jsdom'; 3 | import checkDoctype from '../../../../src/view/drag-drop-context/check-doctype'; 4 | 5 | const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); 6 | 7 | afterEach(() => { 8 | warn.mockClear(); 9 | }); 10 | 11 | it('should pass if using a html doctype', () => { 12 | const jsdom = new JSDOM(`

Hello world

`); 13 | 14 | checkDoctype(jsdom.window.document); 15 | 16 | expect(warn).not.toHaveBeenCalled(); 17 | }); 18 | 19 | it('should fail if there is no doctype', () => { 20 | const jsdom = new JSDOM(`Hello world`); 21 | 22 | checkDoctype(jsdom.window.document); 23 | 24 | expect(warn).toHaveBeenCalled(); 25 | }); 26 | 27 | it('should fail if there is a non-html5 doctype', () => { 28 | // HTML 4.01 Strict 29 | const jsdom = new JSDOM( 30 | `Hello world`, 31 | ); 32 | 33 | checkDoctype(jsdom.window.document); 34 | 35 | expect(warn).toHaveBeenCalled(); 36 | }); 37 | -------------------------------------------------------------------------------- /test/unit/integration/drag-drop-context/unmount.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import DragDropContext from '../../../../src/view/drag-drop-context'; 5 | 6 | it('should not throw when unmounting', () => { 7 | const { unmount } = render( 8 | {}}>{null}, 9 | ); 10 | 11 | expect(() => unmount()).not.toThrow(); 12 | }); 13 | 14 | it('should clean up any window event handlers', () => { 15 | jest.spyOn(window, 'addEventListener'); 16 | jest.spyOn(window, 'removeEventListener'); 17 | 18 | const { unmount } = render( 19 | {}}>{null}, 20 | ); 21 | 22 | unmount(); 23 | 24 | expect(window.addEventListener.mock.calls).toHaveLength( 25 | window.removeEventListener.mock.calls.length, 26 | ); 27 | // validation 28 | expect(window.addEventListener).toHaveBeenCalled(); 29 | expect(window.removeEventListener).toHaveBeenCalled(); 30 | }); 31 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/keyboard-sensor/no-click-blocking.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render, createEvent, fireEvent } from '@testing-library/react'; 4 | import App from '../../util/app'; 5 | import { simpleLift, keyboard } from '../../util/controls'; 6 | 7 | jest.useFakeTimers(); 8 | 9 | it('should not prevent clicks after a drag', () => { 10 | // clearing any pending listeners that have leaked from other tests 11 | fireEvent.click(window); 12 | 13 | const onDragStart = jest.fn(); 14 | const onDragEnd = jest.fn(); 15 | const { getByText } = render( 16 | , 17 | ); 18 | const handle: HTMLElement = getByText('item: 0'); 19 | 20 | simpleLift(keyboard, handle); 21 | 22 | // flush start timer 23 | jest.runOnlyPendingTimers(); 24 | expect(onDragStart).toHaveBeenCalled(); 25 | keyboard.drop(handle); 26 | 27 | const event: Event = createEvent.click(handle); 28 | fireEvent(handle, event); 29 | 30 | // click not blocked 31 | expect(event.defaultPrevented).toBe(false); 32 | expect(onDragEnd).toHaveBeenCalled(); 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/keyboard-sensor/prevent-keyboard-scroll.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { createEvent, fireEvent, render } from '@testing-library/react'; 4 | import * as keyCodes from '../../../../../src/view/key-codes'; 5 | import App from '../../util/app'; 6 | import { simpleLift, keyboard } from '../../util/controls'; 7 | import { isDragging } from '../../util/helpers'; 8 | 9 | it('should prevent using keyboard keys that modify scroll', () => { 10 | const keys: number[] = [ 11 | keyCodes.pageUp, 12 | keyCodes.pageDown, 13 | keyCodes.home, 14 | keyCodes.end, 15 | ]; 16 | const { getByText } = render(); 17 | const handle: HTMLElement = getByText('item: 0'); 18 | 19 | simpleLift(keyboard, handle); 20 | 21 | keys.forEach((keyCode: number) => { 22 | const event: Event = createEvent.keyDown(handle, { keyCode }); 23 | fireEvent(handle, event); 24 | 25 | expect(event.defaultPrevented).toBe(true); 26 | expect(isDragging(handle)).toBe(true); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/keyboard-sensor/prevent-standard-keys-while-dragging.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { createEvent, fireEvent, render } from '@testing-library/react'; 4 | import * as keyCodes from '../../../../../src/view/key-codes'; 5 | import App from '../../util/app'; 6 | import { isDragging } from '../../util/helpers'; 7 | import { simpleLift, keyboard } from '../../util/controls'; 8 | 9 | it('should prevent enter or tab being pressed during a drag', () => { 10 | const { getByText } = render(); 11 | const handle: HTMLElement = getByText('item: 0'); 12 | 13 | simpleLift(keyboard, handle); 14 | expect(isDragging(handle)).toBe(true); 15 | 16 | [keyCodes.enter, keyCodes.tab].forEach((keyCode: number) => { 17 | const event: Event = createEvent.keyDown(handle, { keyCode }); 18 | fireEvent(handle, event); 19 | expect(event.defaultPrevented).toBe(true); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/keyboard-sensor/starting-a-drag.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render, createEvent, fireEvent } from '@testing-library/react'; 4 | import App from '../../util/app'; 5 | import { isDragging } from '../../util/helpers'; 6 | import * as keyCodes from '../../../../../src/view/key-codes'; 7 | 8 | it('should prevent the default keyboard action when lifting', () => { 9 | const { getByText } = render(); 10 | const handle: HTMLElement = getByText('item: 0'); 11 | 12 | const event: Event = createEvent.keyDown(handle, { keyCode: keyCodes.space }); 13 | fireEvent(handle, event); 14 | 15 | expect(isDragging(handle)).toBe(true); 16 | expect(event.defaultPrevented).toBe(true); 17 | }); 18 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/mouse-sensor/prevent-standard-keys-while-dragging.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { createEvent, fireEvent, render } from '@testing-library/react'; 4 | import * as keyCodes from '../../../../../src/view/key-codes'; 5 | import App from '../../util/app'; 6 | import { isDragging } from '../../util/helpers'; 7 | import { simpleLift, mouse } from '../../util/controls'; 8 | 9 | it('should prevent enter or tab being pressed during a drag', () => { 10 | const { getByText } = render(); 11 | const handle: HTMLElement = getByText('item: 0'); 12 | 13 | simpleLift(mouse, handle); 14 | expect(isDragging(handle)).toBe(true); 15 | 16 | [keyCodes.enter, keyCodes.tab].forEach((keyCode: number) => { 17 | const event: Event = createEvent.keyDown(handle, { keyCode }); 18 | fireEvent(handle, event); 19 | expect(event.defaultPrevented).toBe(true); 20 | expect(isDragging(handle)).toBe(true); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/sensor-marshal/force-releasing-locks.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { invariant } from '../../../../../src/invariant'; 5 | import type { 6 | SensorAPI, 7 | Sensor, 8 | PreDragActions, 9 | } from '../../../../../src/types'; 10 | import App from '../../util/app'; 11 | 12 | it('should correctly state whether a lock is claimed', () => { 13 | let first: SensorAPI; 14 | let second: SensorAPI; 15 | const a: Sensor = (value: SensorAPI) => { 16 | first = value; 17 | }; 18 | const b: Sensor = (value: SensorAPI) => { 19 | second = value; 20 | }; 21 | const onForceStop = jest.fn(); 22 | 23 | render( 24 | 25 | 26 | , 27 | ); 28 | invariant(first); 29 | invariant(second); 30 | 31 | const preDrag: ?PreDragActions = first.tryGetLock('0', onForceStop); 32 | expect(preDrag).toBeTruthy(); 33 | expect(second.isLockClaimed()).toBe(true); 34 | 35 | second.tryReleaseLock(); 36 | expect(onForceStop).toHaveBeenCalled(); 37 | // lock is gone 38 | expect(second.isLockClaimed()).toBe(false); 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/sensor-marshal/is-lock-claimed.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { invariant } from '../../../../../src/invariant'; 5 | import type { 6 | SensorAPI, 7 | Sensor, 8 | PreDragActions, 9 | } from '../../../../../src/types'; 10 | import App from '../../util/app'; 11 | 12 | it('should correctly state whether a lock is claimed', () => { 13 | let first: SensorAPI; 14 | let second: SensorAPI; 15 | const a: Sensor = (value: SensorAPI) => { 16 | first = value; 17 | }; 18 | const b: Sensor = (value: SensorAPI) => { 19 | second = value; 20 | }; 21 | 22 | render( 23 | 24 | 25 | , 26 | ); 27 | invariant(first && second); 28 | 29 | // both sensors know that the lock is not claimed 30 | expect(first.isLockClaimed()).toBe(false); 31 | expect(second.isLockClaimed()).toBe(false); 32 | 33 | const preDrag: ?PreDragActions = first.tryGetLock('0'); 34 | expect(preDrag).toBeTruthy(); 35 | 36 | // both sensors can know if the lock is claimed 37 | expect(first.isLockClaimed()).toBe(true); 38 | expect(second.isLockClaimed()).toBe(true); 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/sensor-marshal/lock-context-isolation.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { invariant } from '../../../../../src/invariant'; 5 | import type { SensorAPI, Sensor } from '../../../../../src/types'; 6 | import App from '../../util/app'; 7 | 8 | it('should allow different locks in different DragDropContexts', () => { 9 | let first: SensorAPI; 10 | let second: SensorAPI; 11 | 12 | const a: Sensor = (value: SensorAPI) => { 13 | first = value; 14 | }; 15 | const b: Sensor = (value: SensorAPI) => { 16 | second = value; 17 | }; 18 | 19 | const { getAllByText } = render( 20 | 21 | 22 | 23 | , 24 | ); 25 | 26 | const items: HTMLElement[] = getAllByText('item: 0'); 27 | expect(items).toHaveLength(2); 28 | const [inFirst, inSecond] = items; 29 | expect(inFirst).not.toBe(inSecond); 30 | 31 | // each sensor can get a different lock 32 | invariant(first, 'expected first to be set'); 33 | invariant(second, 'expected second to be set'); 34 | expect(first.tryGetLock('0')).toBeTruthy(); 35 | expect(second.tryGetLock('0')).toBeTruthy(); 36 | }); 37 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/sensor-marshal/no-double-lift.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { invariant } from '../../../../../src/invariant'; 5 | import type { 6 | SensorAPI, 7 | PreDragActions, 8 | SnapDragActions, 9 | Sensor, 10 | } from '../../../../../src/types'; 11 | import App from '../../util/app'; 12 | 13 | it('should not allow double lifting', () => { 14 | let api: SensorAPI; 15 | const a: Sensor = (value: SensorAPI) => { 16 | api = value; 17 | }; 18 | render(); 19 | invariant(api, 'expected first to be set'); 20 | 21 | const preDrag: ?PreDragActions = api.tryGetLock('0'); 22 | invariant(preDrag); 23 | // it is currently active 24 | expect(preDrag.isActive()).toBe(true); 25 | 26 | const drag: SnapDragActions = preDrag.snapLift(); 27 | 28 | expect(() => preDrag.fluidLift({ x: 0, y: 0 })).toThrow(); 29 | // original lock is gone 30 | expect(drag.isActive()).toBe(false); 31 | 32 | // yolo 33 | expect(() => preDrag.snapLift()).toThrow(); 34 | }); 35 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/shared-behaviours/cannot-start-when-disabled.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { isDragging } from '../../util/helpers'; 5 | import App, { type Item } from '../../util/app'; 6 | import { forEachSensor, type Control, simpleLift } from '../../util/controls'; 7 | 8 | forEachSensor((control: Control) => { 9 | it('should not start a drag if disabled', () => { 10 | const items: Item[] = [{ id: '0', isEnabled: false }]; 11 | 12 | const { getByText } = render(); 13 | const handle: HTMLElement = getByText('item: 0'); 14 | 15 | simpleLift(control, handle); 16 | 17 | expect(isDragging(handle)).toBe(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/shared-behaviours/cannot-start-when-something-else-has-lock.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { invariant } from '../../../../../src/invariant'; 5 | import { isDragging } from '../../util/helpers'; 6 | import App from '../../util/app'; 7 | import type { SensorAPI } from '../../../../../src/types'; 8 | import { forEachSensor, type Control, simpleLift } from '../../util/controls'; 9 | 10 | forEachSensor((control: Control) => { 11 | it('should not start a drag if another sensor is capturing', () => { 12 | let api: SensorAPI; 13 | function greedy(value: SensorAPI) { 14 | api = value; 15 | } 16 | const { getByText } = render(); 17 | const handle: HTMLElement = getByText('item: 0'); 18 | 19 | invariant(api, 'Expected function to be set'); 20 | api.tryGetLock('0'); 21 | 22 | // won't be able to lift as the lock is already claimed 23 | simpleLift(control, handle); 24 | 25 | expect(isDragging(handle)).toBe(false); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/shared-behaviours/cannot-start-when-unmounted.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { isDragging } from '../../util/helpers'; 5 | import App from '../../util/app'; 6 | import { forEachSensor, type Control, simpleLift } from '../../util/controls'; 7 | 8 | forEachSensor((control: Control) => { 9 | it('should not allow starting after the handle is unmounted', () => { 10 | const { getByText, unmount } = render(); 11 | const handle: HTMLElement = getByText('item: 0'); 12 | 13 | unmount(); 14 | 15 | simpleLift(control, handle); 16 | 17 | expect(isDragging(handle)).toBe(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/shared-behaviours/disable-default-sensors.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { isDragging } from '../../util/helpers'; 5 | import App from '../../util/app'; 6 | import { forEachSensor, type Control, simpleLift } from '../../util/controls'; 7 | 8 | forEachSensor((control: Control) => { 9 | it('should be able to start a drag if default sensors is disabled', () => { 10 | const { getByText } = render(); 11 | const handle: HTMLElement = getByText('item: 0'); 12 | 13 | simpleLift(control, handle); 14 | expect(isDragging(handle)).toBe(false); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/shared-behaviours/nested-handles.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { forEachSensor, type Control, simpleLift } from '../../util/controls'; 5 | import { isDragging } from '../../util/helpers'; 6 | import Board from '../../util/board'; 7 | 8 | forEachSensor((control: Control) => { 9 | it('should not start a drag on a parent if a child drag handle has already received the event', () => { 10 | const { getByTestId } = render(); 11 | const cardHandle: HTMLElement = getByTestId('inhome1'); 12 | const columnHandle: HTMLElement = getByTestId('home'); 13 | 14 | simpleLift(control, cardHandle); 15 | 16 | expect(isDragging(cardHandle)).toBe(true); 17 | expect(isDragging(columnHandle)).toBe(false); 18 | }); 19 | it('should start a drag on a pare~nt the event is trigged on the parent', () => { 20 | const { getByTestId } = render(); 21 | const cardHandle: HTMLElement = getByTestId('inhome1'); 22 | const columnHandle: HTMLElement = getByTestId('home'); 23 | 24 | simpleLift(control, columnHandle); 25 | 26 | expect(isDragging(columnHandle)).toBe(true); 27 | expect(isDragging(cardHandle)).toBe(false); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/shared-behaviours/parent-rendering-should-not-kill-drag.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { isDragging } from '../../util/helpers'; 5 | import App from '../../util/app'; 6 | import { forEachSensor, simpleLift, type Control } from '../../util/controls'; 7 | 8 | forEachSensor((control: Control) => { 9 | it('should not abort a drag if a parent render occurs', () => { 10 | const { getByText, rerender } = render(); 11 | const handle: HTMLElement = getByText('item: 0'); 12 | 13 | simpleLift(control, handle); 14 | expect(isDragging(handle)).toBe(true); 15 | 16 | rerender(); 17 | 18 | // handle element is unchanged 19 | expect(getByText('item: 0')).toBe(handle); 20 | // it is still dragging 21 | expect(isDragging(handle)).toBe(true); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/touch-sensor/click-blocking.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { fireEvent, render, createEvent } from '@testing-library/react'; 4 | import { act } from 'react-dom/test-utils'; 5 | import App from '../../util/app'; 6 | import { touch, simpleLift } from '../../util/controls'; 7 | 8 | jest.useFakeTimers(); 9 | 10 | it('should block a click after a drag', () => { 11 | const { getByText } = render(); 12 | const handle: HTMLElement = getByText('item: 0'); 13 | 14 | simpleLift(touch, handle); 15 | act(() => touch.drop(handle)); 16 | 17 | const click: Event = createEvent.click(handle); 18 | fireEvent(handle, click); 19 | 20 | expect(click.defaultPrevented).toBe(true); 21 | }); 22 | 23 | it('should not block a click after an aborted pending drag', () => { 24 | const onDragStart = jest.fn(); 25 | const { getByText } = render(); 26 | const handle: HTMLElement = getByText('item: 0'); 27 | 28 | // aborted before getting to a drag 29 | touch.preLift(handle); 30 | act(() => touch.cancel(handle)); 31 | 32 | const click: Event = createEvent.click(handle); 33 | fireEvent(handle, click); 34 | 35 | expect(click.defaultPrevented).toBe(false); 36 | expect(onDragStart).not.toHaveBeenCalled(); 37 | }); 38 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/touch-sensor/context-menu-opt-out.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { fireEvent, render } from '@testing-library/react'; 4 | import App from '../../util/app'; 5 | import { touch } from '../../util/controls'; 6 | import { isDragging } from '../../util/helpers'; 7 | 8 | jest.useFakeTimers(); 9 | 10 | it('should opt of a context menu', () => { 11 | const { getByText } = render(); 12 | const handle: HTMLElement = getByText('item: 0'); 13 | 14 | touch.preLift(handle); 15 | 16 | // prevented during a pending drag 17 | const first: Event = new Event('contextmenu', { 18 | bubbles: true, 19 | cancelable: true, 20 | }); 21 | fireEvent(handle, first); 22 | expect(first.defaultPrevented).toBe(true); 23 | 24 | touch.lift(handle); 25 | 26 | // prevented during a drag 27 | const second: Event = new Event('contextmenu', { 28 | bubbles: true, 29 | cancelable: true, 30 | }); 31 | fireEvent(handle, second); 32 | expect(second.defaultPrevented).toBe(true); 33 | 34 | expect(isDragging(handle)).toBe(true); 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/integration/drag-handle/touch-sensor/unmounted-while-pending-timer-running.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import App from '../../util/app'; 5 | import { isDragging } from '../../util/helpers'; 6 | import { touch } from '../../util/controls'; 7 | import { noop } from '../../../../../src/empty'; 8 | 9 | jest.useFakeTimers(); 10 | 11 | it('should cancel a pending drag when unmounted', () => { 12 | const warn = jest.spyOn(console, 'warn').mockImplementation(noop); 13 | const { getByText, unmount } = render(); 14 | const handle: HTMLElement = getByText('item: 0'); 15 | 16 | touch.preLift(handle); 17 | 18 | unmount(); 19 | 20 | // finish lift timer 21 | jest.runOnlyPendingTimers(); 22 | 23 | expect(warn).not.toHaveBeenCalled(); 24 | expect(isDragging(handle)).toBe(false); 25 | warn.mockRestore(); 26 | }); 27 | -------------------------------------------------------------------------------- /test/unit/state/auto-scroll/fluid-scroller/util/drag-to.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { 4 | Viewport, 5 | DragImpact, 6 | DraggingState, 7 | DroppableDimension, 8 | DimensionMap, 9 | } from '../../../../../../src/types'; 10 | import patchDimensionMap from '../../../../../../src/state/patch-dimension-map'; 11 | 12 | type DragToArgs = {| 13 | selection: Position, 14 | viewport: Viewport, 15 | state: Object, 16 | impact?: DragImpact, 17 | droppable?: DroppableDimension, 18 | |}; 19 | 20 | export default ({ 21 | selection, 22 | viewport, 23 | // seeding that we are over the home droppable 24 | impact, 25 | state, 26 | droppable, 27 | }: DragToArgs): DraggingState => { 28 | const base: DraggingState = state.dragging( 29 | state.preset.inHome1.descriptor.id, 30 | selection, 31 | viewport, 32 | ); 33 | 34 | const dimensions: DimensionMap = (() => { 35 | if (!droppable) { 36 | return base.dimensions; 37 | } 38 | return patchDimensionMap(base.dimensions, droppable); 39 | })(); 40 | 41 | return { 42 | ...base, 43 | // add impact if needed 44 | impact: impact || base.impact, 45 | // add droppable if needed 46 | dimensions, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /test/unit/state/auto-scroll/fluid-scroller/util/for-each.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { vertical, horizontal } from '../../../../../../src/state/axis'; 3 | import type { Axis } from '../../../../../../src/types'; 4 | import { getPreset } from '../../../../../util/dimension'; 5 | import getSimpleStatePreset from '../../../../../util/get-simple-state-preset'; 6 | 7 | export type BlockFnArgs = {| 8 | axis: Axis, 9 | preset: Object, 10 | state: Object, 11 | |}; 12 | 13 | type BlockFn = (args: BlockFnArgs) => void; 14 | 15 | export default (block: BlockFn) => { 16 | [vertical, horizontal].forEach((axis: Axis) => { 17 | describe(`on the ${axis.direction} axis`, () => { 18 | beforeEach(() => { 19 | requestAnimationFrame.reset(); 20 | jest.useFakeTimers(); 21 | }); 22 | afterEach(() => { 23 | jest.useRealTimers(); 24 | }); 25 | 26 | afterAll(() => { 27 | requestAnimationFrame.reset(); 28 | }); 29 | 30 | const preset = getPreset(axis); 31 | const state = getSimpleStatePreset(axis); 32 | 33 | block({ axis, preset, state }); 34 | }); 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /test/unit/state/auto-scroll/fluid-scroller/util/get-args-mock.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { DroppableId } from '../../../../../../src/types'; 4 | 5 | // Similiar to PublicArgs 6 | type Result = {| 7 | scrollWindow: JestMockFn<[Position], void>, 8 | scrollDroppable: JestMockFn<[DroppableId, Position], void>, 9 | |}; 10 | 11 | export default (): Result => { 12 | const scrollWindow: JestMockFn<[Position], void> = jest.fn(); 13 | const scrollDroppable: JestMockFn<[DroppableId, Position], void> = jest.fn(); 14 | 15 | return { 16 | scrollWindow, 17 | scrollDroppable, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /test/unit/state/auto-scroll/fluid-scroller/util/viewport.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getRect } from 'css-box-model'; 3 | import type { Viewport } from '../../../../../../src/types'; 4 | import { origin } from '../../../../../../src/state/position'; 5 | import { createViewport } from '../../../../../util/viewport'; 6 | 7 | export const windowScrollSize = { 8 | scrollHeight: 2000, 9 | scrollWidth: 1600, 10 | }; 11 | export const scrollableViewport: Viewport = createViewport({ 12 | frame: getRect({ 13 | top: 0, 14 | left: 0, 15 | right: 800, 16 | bottom: 1000, 17 | }), 18 | scrollHeight: windowScrollSize.scrollHeight, 19 | scrollWidth: windowScrollSize.scrollWidth, 20 | scroll: origin, 21 | }); 22 | 23 | export const unscrollableViewport: Viewport = createViewport({ 24 | frame: getRect({ 25 | top: 0, 26 | left: 0, 27 | right: 800, 28 | bottom: 1000, 29 | }), 30 | scrollHeight: 1000, 31 | scrollWidth: 800, 32 | scroll: origin, 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/state/droppable/clip.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getRect, type Spacing } from 'css-box-model'; 3 | import clip from '../../../../src/state/droppable/util/clip'; 4 | import { offsetByPosition } from '../../../../src/state/spacing'; 5 | 6 | it('should select clip a subject in a frame', () => { 7 | const subject: Spacing = { 8 | top: 0, 9 | left: 0, 10 | right: 100, 11 | bottom: 100, 12 | }; 13 | const frame: Spacing = { 14 | top: 20, 15 | left: 20, 16 | right: 50, 17 | bottom: 50, 18 | }; 19 | 20 | expect(clip(frame, subject)).toEqual(getRect(frame)); 21 | }); 22 | 23 | it('should return null when the subject it outside the frame on any side', () => { 24 | const frame: Spacing = { 25 | top: 0, 26 | left: 0, 27 | right: 100, 28 | bottom: 100, 29 | }; 30 | const outside: Spacing[] = [ 31 | // top 32 | offsetByPosition(frame, { x: 0, y: -200 }), 33 | // right 34 | offsetByPosition(frame, { x: 200, y: 0 }), 35 | // bottom 36 | offsetByPosition(frame, { x: 0, y: 200 }), 37 | // left 38 | offsetByPosition(frame, { x: -200, y: 0 }), 39 | ]; 40 | 41 | outside.forEach((subject: Spacing) => { 42 | expect(clip(frame, subject)).toEqual(null); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/unit/state/droppable/is-home-of.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getPreset } from '../../../util/dimension'; 3 | import isHomeOf from '../../../../src/state/droppable/is-home-of'; 4 | 5 | const preset = getPreset(); 6 | 7 | it('should return true if destination is home of draggable', () => { 8 | expect(isHomeOf(preset.inHome1, preset.home)).toBe(true); 9 | expect(isHomeOf(preset.inHome2, preset.home)).toBe(true); 10 | expect(isHomeOf(preset.inForeign1, preset.foreign)).toBe(true); 11 | }); 12 | 13 | it('should return false if destination is not home of draggable', () => { 14 | expect(isHomeOf(preset.inForeign1, preset.home)).toBe(false); 15 | expect(isHomeOf(preset.inHome1, preset.foreign)).toBe(false); 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/state/droppable/what-is-dragged-over.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import noImpact from '../../../../src/state/no-impact'; 3 | import whatIsDraggedOver from '../../../../src/state/droppable/what-is-dragged-over'; 4 | import type { 5 | DraggableLocation, 6 | DragImpact, 7 | CombineImpact, 8 | } from '../../../../src/types'; 9 | 10 | it('should return the droppableId of a reorder impact', () => { 11 | const destination: DraggableLocation = { 12 | droppableId: 'droppable', 13 | index: 0, 14 | }; 15 | const impact: DragImpact = { 16 | ...noImpact, 17 | at: { 18 | type: 'REORDER', 19 | destination, 20 | }, 21 | }; 22 | expect(whatIsDraggedOver(impact)).toEqual(destination.droppableId); 23 | }); 24 | 25 | it('should return the droppableId from a merge impact', () => { 26 | const at: CombineImpact = { 27 | type: 'COMBINE', 28 | combine: { 29 | draggableId: 'draggable', 30 | droppableId: 'droppable', 31 | }, 32 | }; 33 | const impact: DragImpact = { 34 | ...noImpact, 35 | at, 36 | }; 37 | expect(whatIsDraggedOver(impact)).toEqual(at.combine.droppableId); 38 | }); 39 | 40 | it('should return null when there is no destination or merge impact', () => { 41 | expect(whatIsDraggedOver(noImpact)).toEqual(null); 42 | }); 43 | -------------------------------------------------------------------------------- /test/unit/state/get-drag-impact/util/get-combine-threshold.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { Axis, DraggableDimension } from '../../../../../src/types'; 4 | import { patch } from '../../../../../src/state/position'; 5 | import { combineThresholdDivisor } from '../../../../../src/state/get-drag-impact/get-combine-impact'; 6 | 7 | export function getThreshold( 8 | axis: Axis, 9 | draggable: DraggableDimension, 10 | ): Position { 11 | return patch( 12 | axis.line, 13 | draggable.page.borderBox[axis.size] / combineThresholdDivisor, 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /test/unit/state/get-droppable-over/is-disabled.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DroppableDimensionMap, DroppableId } from '../../../../src/types'; 3 | import getDroppableOver from '../../../../src/state/get-droppable-over'; 4 | import { disableDroppable, getPreset } from '../../../util/dimension'; 5 | 6 | const preset = getPreset(); 7 | 8 | it('should not consider lists that are disabled', () => { 9 | const withDisabled: DroppableDimensionMap = { 10 | ...preset.droppables, 11 | [preset.home.descriptor.id]: disableDroppable(preset.home), 12 | }; 13 | 14 | const whileEnabled: ?DroppableId = getDroppableOver({ 15 | pageBorderBox: preset.inHome1.page.borderBox, 16 | draggable: preset.inHome1, 17 | droppables: preset.droppables, 18 | }); 19 | const whileDisabled: ?DroppableId = getDroppableOver({ 20 | pageBorderBox: preset.inHome1.page.borderBox, 21 | draggable: preset.inHome1, 22 | droppables: withDisabled, 23 | }); 24 | 25 | expect(whileEnabled).toBe(preset.home.descriptor.id); 26 | expect(whileDisabled).toBe(null); 27 | }); 28 | -------------------------------------------------------------------------------- /test/unit/state/get-droppable-over/is-over-nothing.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DroppableId } from '../../../../src/types'; 3 | import getDroppableOver from '../../../../src/state/get-droppable-over'; 4 | import { getPreset } from '../../../util/dimension'; 5 | import { offsetRectByPosition } from '../../../../src/state/rect'; 6 | 7 | const preset = getPreset(); 8 | 9 | it('should return null when over nothing', () => { 10 | const result: ?DroppableId = getDroppableOver({ 11 | pageBorderBox: offsetRectByPosition(preset.inHome1.page.borderBox, { 12 | x: 10000, 13 | y: 10000, 14 | }), 15 | draggable: preset.inHome1, 16 | droppables: preset.droppables, 17 | }); 18 | 19 | expect(result).toBe(null); 20 | }); 21 | -------------------------------------------------------------------------------- /test/unit/state/is-within.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import isWithin from '../../../src/state/is-within'; 3 | 4 | describe('is within', () => { 5 | const lowerBound: number = 5; 6 | const upperBound: number = 10; 7 | const execute = isWithin(5, 10); 8 | 9 | it('should return true when the value is between the bounds', () => { 10 | expect(execute(lowerBound + 1)).toBe(true); 11 | }); 12 | 13 | it('should return true when the value is equal to the lower bound', () => { 14 | expect(execute(lowerBound)).toBe(true); 15 | }); 16 | 17 | it('should return true when the value is equal to the upper bound', () => { 18 | expect(execute(upperBound)).toBe(true); 19 | }); 20 | 21 | it('should return false when the value is less then the lower bound', () => { 22 | expect(execute(lowerBound - 1)).toBe(false); 23 | }); 24 | 25 | it('should return false when the value is greater than the upper bound', () => { 26 | expect(execute(upperBound + 1)).toBe(false); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/unit/state/middleware/responders/util/get-announce-stub.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Announce } from '../../../../../../src/types'; 3 | 4 | export default (): Announce => jest.fn(); 5 | -------------------------------------------------------------------------------- /test/unit/state/middleware/responders/util/get-completed-with-result.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | DropResult, 4 | State, 5 | CompletedDrag, 6 | } from '../../../../../../src/types'; 7 | import { invariant } from '../../../../../../src/invariant'; 8 | 9 | export default (result: DropResult, state: State): CompletedDrag => { 10 | invariant( 11 | state.phase === 'DRAGGING', 12 | `This is just a simple helper. 13 | Unsupported phase: ${state.phase}`, 14 | ); 15 | 16 | return { 17 | afterCritical: state.afterCritical, 18 | critical: state.critical, 19 | result, 20 | impact: state.impact, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /test/unit/state/middleware/responders/util/get-responders-stub.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Responders } from '../../../../../../src/types'; 3 | 4 | export default (): Responders => ({ 5 | onBeforeDragStart: jest.fn(), 6 | onDragStart: jest.fn(), 7 | onDragUpdate: jest.fn(), 8 | onDragEnd: jest.fn(), 9 | }); 10 | -------------------------------------------------------------------------------- /test/unit/state/middleware/util/create-store.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import reducer from '../../../../../src/state/reducer'; 4 | import type { Store, Middleware } from '../../../../../src/state/store-types'; 5 | 6 | export default (...middleware: Middleware[]): Store => 7 | createStore(reducer, applyMiddleware(...middleware)); 8 | -------------------------------------------------------------------------------- /test/unit/state/middleware/util/pass-through-middleware.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Action, Middleware } from '../../../../../src/state/store-types'; 3 | 4 | const passThrough = (mock: Function): Middleware => { 5 | const result: Middleware = () => (next: Function) => ( 6 | action: Action, 7 | ): any => { 8 | mock(action); 9 | next(action); 10 | }; 11 | 12 | return result; 13 | }; 14 | 15 | export default passThrough; 16 | -------------------------------------------------------------------------------- /test/unit/state/post-reducer/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/react-beautiful-dnd/b833d66c437b6a96c13591bcc9f7b3f4d2a6170e/test/unit/state/post-reducer/.gitkeep -------------------------------------------------------------------------------- /test/unit/state/publish-while-dragging/nothing-changed.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | DropPendingState, 4 | DraggingState, 5 | CollectingState, 6 | } from '../../../../src/types'; 7 | import { invariant } from '../../../../src/invariant'; 8 | import publish from '../../../../src/state/publish-while-dragging-in-virtual'; 9 | import getStatePreset from '../../../util/get-simple-state-preset'; 10 | import { empty, withVirtuals } from './util'; 11 | 12 | const state = getStatePreset(); 13 | 14 | it('should do not modify the dimensions when nothing has changed', () => { 15 | const original: CollectingState = withVirtuals(state.collecting()); 16 | 17 | const result: DraggingState | DropPendingState = publish({ 18 | state: original, 19 | published: empty, 20 | }); 21 | 22 | invariant(result.phase === 'DRAGGING'); 23 | 24 | // only minor modifications on original 25 | const expected: DraggingState = { 26 | phase: 'DRAGGING', 27 | ...original, 28 | // appeasing flow 29 | // eslint-disable-next-line 30 | phase: 'DRAGGING', 31 | // we force no animation of the moving item 32 | forceShouldAnimate: false, 33 | }; 34 | expect(result).toEqual(expected); 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/state/publish-while-dragging/phase-change.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { invariant } from '../../../../src/invariant'; 4 | import getStatePreset from '../../../util/get-simple-state-preset'; 5 | import type { DropPendingState, DraggingState } from '../../../../src/types'; 6 | import publish from '../../../../src/state/publish-while-dragging-in-virtual'; 7 | import { empty } from './util'; 8 | 9 | const state = getStatePreset(); 10 | 11 | it('should move to the DRAGGING phase if was in the COLLECTING phase', () => { 12 | const result: DraggingState | DropPendingState = publish({ 13 | state: state.collecting(), 14 | published: empty, 15 | }); 16 | 17 | expect(result.phase).toBe('DRAGGING'); 18 | }); 19 | 20 | it('should move into a non-waiting DROP_PENDING phase if was in a DROP_PENDING phase', () => { 21 | const result: DraggingState | DropPendingState = publish({ 22 | state: state.dropPending(), 23 | published: empty, 24 | }); 25 | 26 | expect(result.phase).toBe('DROP_PENDING'); 27 | invariant(result.phase === 'DROP_PENDING'); 28 | expect(result.reason).toBe(state.dropPending().reason); 29 | }); 30 | -------------------------------------------------------------------------------- /test/unit/view/connected-draggable/util/get-dragging-map-props.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | MappedProps, 4 | MapProps, 5 | DraggingMapProps, 6 | } from '../../../../../src/view/draggable/draggable-types'; 7 | import { invariant } from '../../../../../src/invariant'; 8 | 9 | export default (mapProps: MapProps): DraggingMapProps => { 10 | const mapped: MappedProps = mapProps.mapped; 11 | invariant(mapped.type === 'DRAGGING'); 12 | return mapped; 13 | }; 14 | -------------------------------------------------------------------------------- /test/unit/view/connected-draggable/util/get-own-props.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { OwnProps } from '../../../../../src/view/draggable/draggable-types'; 3 | import type { DraggableDimension } from '../../../../../src/types'; 4 | 5 | export default (dimension: DraggableDimension): OwnProps => ({ 6 | // Public own props 7 | draggableId: dimension.descriptor.id, 8 | index: dimension.descriptor.index, 9 | children: () => null, 10 | 11 | // Private own props 12 | isClone: false, 13 | isEnabled: true, 14 | canDragInteractiveElements: false, 15 | shouldRespectForcePress: true, 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/view/connected-draggable/util/get-secondary-map-props.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | MapProps, 4 | MappedProps, 5 | SecondaryMapProps, 6 | } from '../../../../../src/view/draggable/draggable-types'; 7 | import { invariant } from '../../../../../src/invariant'; 8 | 9 | export default (mapProps: MapProps): SecondaryMapProps => { 10 | const mapped: MappedProps = mapProps.mapped; 11 | invariant(mapped.type === 'SECONDARY'); 12 | return mapped; 13 | }; 14 | -------------------------------------------------------------------------------- /test/unit/view/connected-draggable/util/get-snapshot.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | StateSnapshot, 4 | DropAnimation, 5 | } from '../../../../../src/view/draggable/draggable-types'; 6 | import type { 7 | MovementMode, 8 | DroppableId, 9 | DraggableId, 10 | } from '../../../../../src/types'; 11 | 12 | type GetDraggingSnapshotArgs = {| 13 | mode: MovementMode, 14 | draggingOver: ?DroppableId, 15 | combineWith: ?DraggableId, 16 | dropping: ?DropAnimation, 17 | isClone?: ?boolean, 18 | |}; 19 | 20 | export const getDraggingSnapshot = ({ 21 | mode, 22 | draggingOver, 23 | combineWith, 24 | dropping, 25 | isClone, 26 | }: GetDraggingSnapshotArgs): StateSnapshot => ({ 27 | isDragging: true, 28 | isDropAnimating: Boolean(dropping), 29 | dropAnimation: dropping, 30 | mode, 31 | draggingOver, 32 | combineWith, 33 | combineTargetFor: null, 34 | isClone: Boolean(isClone), 35 | }); 36 | 37 | type GetSecondarySnapshotArgs = {| 38 | combineTargetFor: ?DraggableId, 39 | |}; 40 | 41 | export const getSecondarySnapshot = ({ 42 | combineTargetFor, 43 | }: GetSecondarySnapshotArgs): StateSnapshot => ({ 44 | isDragging: false, 45 | isClone: false, 46 | isDropAnimating: false, 47 | dropAnimation: null, 48 | mode: null, 49 | draggingOver: null, 50 | combineTargetFor, 51 | combineWith: null, 52 | }); 53 | -------------------------------------------------------------------------------- /test/unit/view/connected-droppable/util/get-own-props.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DroppableDimension } from '../../../../../src/types'; 3 | import type { OwnProps } from '../../../../../src/view/droppable/droppable-types'; 4 | import getBodyElement from '../../../../../src/view/get-body-element'; 5 | 6 | export default (dimension: DroppableDimension): OwnProps => ({ 7 | droppableId: dimension.descriptor.id, 8 | type: dimension.descriptor.type, 9 | isDropDisabled: false, 10 | isCombineEnabled: true, 11 | direction: dimension.axis.direction, 12 | ignoreContainerClipping: false, 13 | children: () => null, 14 | getContainerForClone: getBodyElement, 15 | mode: 'standard', 16 | renderClone: null, 17 | }); 18 | -------------------------------------------------------------------------------- /test/unit/view/connected-droppable/util/resting-props.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { MapProps } from '../../../../../src/view/droppable/droppable-types'; 3 | 4 | const restingProps: MapProps = { 5 | placeholder: null, 6 | shouldAnimatePlaceholder: false, 7 | snapshot: { 8 | isDraggingOver: false, 9 | draggingOverWith: null, 10 | draggingFromThisWith: null, 11 | isUsingPlaceholder: false, 12 | }, 13 | useClone: null, 14 | }; 15 | 16 | export default restingProps; 17 | -------------------------------------------------------------------------------- /test/unit/view/connected-droppable/util/with-combine-impact.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DragImpact, Combine } from '../../../../../src/types'; 3 | 4 | export default (impact: DragImpact, combine: Combine): DragImpact => ({ 5 | ...impact, 6 | at: { 7 | type: 'COMBINE', 8 | combine, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /test/unit/view/dimension-marshal/util.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getPreset } from '../../../util/dimension'; 3 | import type { 4 | DimensionMap, 5 | LiftRequest, 6 | Critical, 7 | } from '../../../../src/types'; 8 | 9 | const preset = getPreset(); 10 | 11 | export const defaultRequest: LiftRequest = { 12 | draggableId: preset.inHome1.descriptor.id, 13 | scrollOptions: { 14 | shouldPublishImmediately: false, 15 | }, 16 | }; 17 | 18 | export const critical: Critical = { 19 | draggable: preset.inHome1.descriptor, 20 | droppable: preset.home.descriptor, 21 | }; 22 | 23 | export const justCritical: DimensionMap = { 24 | draggables: { 25 | [preset.inHome1.descriptor.id]: preset.inHome1, 26 | }, 27 | droppables: { 28 | [preset.home.descriptor.id]: preset.home, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /test/unit/view/drag-drop-context/content-security-protection-nonce.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { mount, type ReactWrapper } from 'enzyme'; 4 | import DragDropContext from '../../../../src/view/drag-drop-context'; 5 | import { resetServerContext } from '../../../../src'; 6 | import * as attributes from '../../../../src/view/data-attributes'; 7 | 8 | it('should insert nonce into style tag', () => { 9 | const nonce = 'ThisShouldBeACryptographicallySecurePseudorandomNumber'; 10 | 11 | resetServerContext(); 12 | const wrapper1: ReactWrapper<*> = mount( 13 | {}}> 14 | {null} 15 | , 16 | ); 17 | const styleTag = document.querySelector(`[${attributes.prefix}-always="0"]`); 18 | const nonceAttribute = styleTag ? styleTag.getAttribute('nonce') : ''; 19 | expect(nonceAttribute).toEqual(nonce); 20 | 21 | wrapper1.unmount(); 22 | }); 23 | -------------------------------------------------------------------------------- /test/unit/view/droppable/util/get-stubber.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import type { 4 | Provided, 5 | StateSnapshot, 6 | } from '../../../../../src/view/droppable/droppable-types'; 7 | 8 | export default (mock?: Function = () => {}) => 9 | class Stubber extends React.Component<{ 10 | provided: Provided, 11 | snapshot: StateSnapshot, 12 | }> { 13 | render() { 14 | const { provided, snapshot } = this.props; 15 | mock({ 16 | provided, 17 | snapshot, 18 | }); 19 | return ( 20 |
21 | Hey there 22 | {provided.placeholder} 23 |
24 | ); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /test/unit/view/is-type-of-element/util/get-svg.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // $FlowFixMe - flow does not know what a SVGElement is 3 | export default (doc: HTMLElement): SVGElement => 4 | doc.createElementNS('http://www.w3.org/2000/svg', 'svg'); 5 | -------------------------------------------------------------------------------- /test/unit/view/placeholder/util/data.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Placeholder } from '../../../../../src/types'; 3 | import { getPreset } from '../../../../util/dimension'; 4 | 5 | export const preset = getPreset(); 6 | export const placeholder: Placeholder = preset.inHome1.placeholder; 7 | -------------------------------------------------------------------------------- /test/unit/view/placeholder/util/expect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { PlaceholderStyle } from '../../../../../src/view/placeholder/placeholder-types'; 3 | import { placeholder } from './data'; 4 | 5 | export const expectIsEmpty = (style: PlaceholderStyle) => { 6 | expect(style.width).toBe(0); 7 | expect(style.height).toBe(0); 8 | expect(style.marginTop).toBe(0); 9 | expect(style.marginRight).toBe(0); 10 | expect(style.marginBottom).toBe(0); 11 | expect(style.marginLeft).toBe(0); 12 | }; 13 | 14 | export const expectIsFull = (style: PlaceholderStyle) => { 15 | expect(style.width).toBe(placeholder.client.borderBox.width); 16 | expect(style.height).toBe(placeholder.client.borderBox.height); 17 | expect(style.marginTop).toBe(placeholder.client.margin.top); 18 | expect(style.marginRight).toBe(placeholder.client.margin.right); 19 | expect(style.marginBottom).toBe(placeholder.client.margin.bottom); 20 | expect(style.marginLeft).toBe(placeholder.client.margin.left); 21 | }; 22 | -------------------------------------------------------------------------------- /test/unit/view/placeholder/util/get-placeholder-style.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ReactWrapper } from 'enzyme'; 3 | import type { PlaceholderStyle } from '../../../../../src/view/placeholder/placeholder-types'; 4 | import { placeholder } from './data'; 5 | 6 | export default (wrapper: ReactWrapper<*>): PlaceholderStyle => 7 | wrapper.find(placeholder.tagName).props().style; 8 | -------------------------------------------------------------------------------- /test/unit/view/placeholder/util/placeholder-with-class.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { WithoutMemo } from '../../../../../src/view/placeholder/placeholder'; 4 | import type { Props } from '../../../../../src/view/placeholder/placeholder'; 5 | 6 | // enzyme does not work well with memo, so exporting the non-memo version 7 | // Using PureComponent to match behaviour of React.memo 8 | export default class PlaceholderWithClass extends React.PureComponent { 9 | render() { 10 | return ; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/unit/view/style-marshal/get-styles.spec.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import stylelint from 'stylelint'; 3 | import getStyles, { 4 | type Styles, 5 | } from '../../../../src/view/use-style-marshal/get-styles'; 6 | 7 | const styles: Styles = getStyles('hey'); 8 | 9 | Object.keys(styles).forEach((key: string) => { 10 | it(`should generate valid ${key} styles`, () => 11 | stylelint 12 | .lint({ 13 | code: styles[key], 14 | config: { 15 | // just using the recommended config as it only checks for errors and not formatting 16 | extends: ['stylelint-config-recommended'], 17 | // basic semi colin rules 18 | rules: { 19 | 'no-extra-semicolons': true, 20 | 'declaration-block-semicolon-space-after': 'always-single-line', 21 | }, 22 | }, 23 | }) 24 | .then((result) => { 25 | expect(result.errored).toBe(false); 26 | // asserting that some CSS was actually generated! 27 | // eslint-disable-next-line no-underscore-dangle 28 | expect(result.results[0]._postcssResult.css.length).toBeGreaterThan(1); 29 | })); 30 | }); 31 | -------------------------------------------------------------------------------- /test/util/after-point.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { Axis } from '../../src/types'; 4 | import { add, patch } from '../../src/state/position'; 5 | 6 | export default function afterPoint(axis: Axis, point: Position): Position { 7 | return add(point, patch(axis.line, 1)); 8 | } 9 | 10 | export function afterCrossAxisPoint(axis: Axis, point: Position): Position { 11 | return add(point, patch(axis.crossAxisLine, 1)); 12 | } 13 | -------------------------------------------------------------------------------- /test/util/before-point.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Position } from 'css-box-model'; 3 | import type { Axis } from '../../src/types'; 4 | import { subtract, patch } from '../../src/state/position'; 5 | 6 | export default function beforePoint(axis: Axis, point: Position): Position { 7 | return subtract(point, patch(axis.line, 1)); 8 | } 9 | 10 | export function beforeCrossAxisPoint(axis: Axis, point: Position): Position { 11 | return subtract(point, patch(axis.crossAxisLine, 1)); 12 | } 13 | -------------------------------------------------------------------------------- /test/util/cause-runtime-error.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export function getRuntimeError(): Event { 3 | return new window.ErrorEvent('error', { 4 | error: new Error('non-rbd'), 5 | cancelable: true, 6 | }); 7 | } 8 | 9 | export default function causeRuntimeError() { 10 | window.dispatchEvent(getRuntimeError()); 11 | } 12 | -------------------------------------------------------------------------------- /test/util/clone-impact.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { DragImpact } from '../../src/types'; 3 | 4 | export default (target: DragImpact): DragImpact => 5 | JSON.parse(JSON.stringify(target)); 6 | -------------------------------------------------------------------------------- /test/util/console.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { noop } from '../../src/empty'; 3 | 4 | function withConsole(type: string, fn: () => void, message?: string) { 5 | const mock = jest.spyOn(console, type).mockImplementation(noop); 6 | 7 | fn(); 8 | 9 | expect(mock).toHaveBeenCalled(); 10 | 11 | if (message) { 12 | expect(mock).toHaveBeenCalledWith(expect.stringContaining(message)); 13 | } 14 | 15 | mock.mockReset(); 16 | } 17 | 18 | export const withError = withConsole.bind(null, 'error'); 19 | export const withWarn = withConsole.bind(null, 'warn'); 20 | 21 | function withoutConsole(type: string, fn: () => void) { 22 | const mock = jest.spyOn(console, type).mockImplementation(noop); 23 | 24 | fn(); 25 | 26 | expect(mock).not.toHaveBeenCalled(); 27 | mock.mockReset(); 28 | } 29 | 30 | export const withoutError = withoutConsole.bind(null, 'error'); 31 | export const withoutWarn = withoutConsole.bind(null, 'warn'); 32 | -------------------------------------------------------------------------------- /test/util/create-ref.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default function createRef() { 3 | let ref: ?HTMLElement = null; 4 | 5 | const setRef = (supplied: ?HTMLElement) => { 6 | ref = supplied; 7 | }; 8 | 9 | const getRef = (): ?HTMLElement => ref; 10 | 11 | return { ref, setRef, getRef }; 12 | } 13 | -------------------------------------------------------------------------------- /test/util/force-update.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { ReactWrapper } from 'enzyme'; 3 | 4 | // wrapper.update() no longer forces a render 5 | // instead using wrapper.setProps({}); 6 | // https://github.com/airbnb/enzyme/issues/1245 7 | 8 | export default (wrapper: ReactWrapper<*>) => wrapper.setProps({}); 9 | -------------------------------------------------------------------------------- /test/util/no-after-critical.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { LiftEffect } from '../../src/types'; 3 | import { origin } from '../../src/state/position'; 4 | 5 | const noOnLift: LiftEffect = { 6 | effected: {}, 7 | inVirtualList: false, 8 | displacedBy: { 9 | point: origin, 10 | value: 0, 11 | }, 12 | }; 13 | 14 | export default noOnLift; 15 | -------------------------------------------------------------------------------- /test/util/pass-through-props.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Node } from 'react'; 3 | 4 | type Props = {| 5 | ...T, 6 | children: (value: T) => Node, 7 | |}; 8 | 9 | export default function PassThroughProps(props: Props<*>) { 10 | const { children, ...rest } = props; 11 | return children(rest); 12 | } 13 | -------------------------------------------------------------------------------- /test/util/reorder.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // A little helper function to reorder a list 4 | export default (list: any[], startIndex: number, endIndex: number): any[] => { 5 | const result = Array.from(list); 6 | const [removed] = result.splice(startIndex, 1); 7 | result.splice(endIndex, 0, removed); 8 | 9 | return result; 10 | }; 11 | -------------------------------------------------------------------------------- /test/util/set-window-scroll-size.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type Args = {| 4 | scrollHeight: number, 5 | scrollWidth: number, 6 | |}; 7 | 8 | const setWindowScrollSize = ({ scrollHeight, scrollWidth }: Args): void => { 9 | const el: ?HTMLElement = document.documentElement; 10 | 11 | if (!el) { 12 | throw new Error('Unable to find document element'); 13 | } 14 | 15 | el.scrollHeight = scrollHeight; 16 | el.scrollWidth = scrollWidth; 17 | }; 18 | 19 | const original: Args = (() => { 20 | const el: ?HTMLElement = document.documentElement; 21 | 22 | if (!el) { 23 | throw new Error('Unable to find document element'); 24 | } 25 | 26 | return { 27 | scrollWidth: el.scrollWidth, 28 | scrollHeight: el.scrollHeight, 29 | }; 30 | })(); 31 | 32 | export const resetWindowScrollSize = () => setWindowScrollSize(original); 33 | 34 | export default setWindowScrollSize; 35 | -------------------------------------------------------------------------------- /test/util/set-window-scroll.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Position } from 'css-box-model'; 3 | 4 | type Options = {| 5 | shouldPublish: boolean, 6 | |}; 7 | 8 | const defaultOptions: Options = { 9 | shouldPublish: true, 10 | }; 11 | 12 | const setWindowScroll = ( 13 | point: Position, 14 | options?: Options = defaultOptions, 15 | ) => { 16 | window.pageXOffset = point.x; 17 | window.pageYOffset = point.y; 18 | 19 | if (options.shouldPublish) { 20 | window.dispatchEvent(new Event('scroll')); 21 | } 22 | }; 23 | 24 | const original: Position = { 25 | x: window.pageXOffset, 26 | y: window.pageYOffset, 27 | }; 28 | 29 | export const resetWindowScroll = () => setWindowScroll(original); 30 | 31 | export default setWindowScroll; 32 | -------------------------------------------------------------------------------- /test/util/spacing.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Spacing } from 'css-box-model'; 3 | 4 | export const expandBySpacing = ( 5 | spacing1: Spacing, 6 | spacing2: Spacing, 7 | ): Spacing => ({ 8 | // pulling back to increase size 9 | top: spacing1.top - spacing2.top, 10 | left: spacing1.left - spacing2.left, 11 | // pushing forward to increase size 12 | bottom: spacing1.bottom + spacing2.bottom, 13 | right: spacing1.right + spacing2.right, 14 | }); 15 | 16 | export const shrinkBySpacing = ( 17 | spacing1: Spacing, 18 | spacing2: Spacing, 19 | ): Spacing => ({ 20 | // pushing forward to descrease size 21 | top: spacing1.top + spacing2.top, 22 | left: spacing1.left + spacing2.left, 23 | // pulling backwards to descrease size 24 | bottom: spacing1.bottom - spacing2.bottom, 25 | right: spacing1.right - spacing2.right, 26 | }); 27 | -------------------------------------------------------------------------------- /test/util/try-clean-prototype-stubs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default () => { 3 | // clean up any stubs 4 | if (Element.prototype.getBoundingClientRect.mockRestore) { 5 | Element.prototype.getBoundingClientRect.mockRestore(); 6 | } 7 | if (window.getComputedStyle.mockRestore) { 8 | window.getComputedStyle.mockRestore(); 9 | } 10 | }; 11 | --------------------------------------------------------------------------------