diff --git a/.babelrc b/.babelrc deleted file mode 100644 index d38c7e676..000000000 --- a/.babelrc +++ /dev/null @@ -1,34 +0,0 @@ -{ - "env": { - "development": { - "presets": ["es2015", "react", "react-hmre", "stage-0", "stage-1", "stage-2"] - }, - "commonjs": { - "presets": [ - "es2015", - "react", - "stage-0", - "stage-1" - ] - }, - "es6": { - "presets": [ - "es2015-rollup", - "react", - "stage-0", - "stage-1" - ], - "plugins": [ - "transform-runtime" - ] - }, - "production": { - "presets": ["es2015", "react", "stage-0", "stage-1", "stage-2"], - "plugins": ["transform-runtime", "transform-object-assign"] - }, - "testing": { - "presets": ["es2015", "react", "stage-0", "stage-1", "stage-2"], - "plugins": ["transform-object-assign"] - } - } -} diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json new file mode 100644 index 000000000..f6df45078 --- /dev/null +++ b/.codesandbox/ci.json @@ -0,0 +1,3 @@ +{ + "sandboxes": ["react", "o104x95y86"] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c9fe4a74f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# editorconfig.org +root = true + +[*] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +# Markdown syntax specifies that trailing whitespaces can be meaningful, +# so let’s not trim those. e.g. 2 trailing spaces = linebreak (
) +# See https://daringfireball.net/projects/markdown/syntax#p +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a75de1f3a..000000000 --- a/.eslintrc +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "react-app", - "rules": { - "prefer-const": ["error"], - "comma-dangle": ["error", "always-multiline"], - "indent": ["error", 2, {"SwitchCase": 1}], - "semi": ["error", "always"], - "no-mixed-operators": [ - "warn", - { - "groups": [ - ['&', '|', '^', '~', '<<', '>>', '>>>'], - ['==', '!=', '===', '!==', '>', '>=', '<', '<='], - ['in', 'instanceof'] - ], - "allowSamePrecedence": true - } - ], - "quotes": ["error", "single"] - } -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..a5bd1594c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "extends": ["plugin:shopify/react", "plugin:prettier/recommended"], + "rules": { + "no-process-env": "off", + "no-lonely-if": "off", + "no-undefined": "off", + "no-param-reassign": "off", + "no-mixed-operators": "off", + "no-misleading-character-class": "off", + "require-atomic-updates": "off", + "prefer-object-spread": "off", + "lines-around-comment": "off", + "function-paren-newline": "off", + "promise/catch-or-return": "off", + "react/forbid-prop-types": "off", + "react/jsx-filename-extension": "off", + "react/no-unused-prop-types": "off", + "react/no-string-refs": 1, + "react/no-deprecated": 1, + "shopify/binary-assignment-parens": "off", + "sort-class-members/sort-class-members": "off" + } +} diff --git a/.github/assets/dnd-kit-banner.svg b/.github/assets/dnd-kit-banner.svg new file mode 100644 index 000000000..41f967780 --- /dev/null +++ b/.github/assets/dnd-kit-banner.svg @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/assets/react-sortable-hoc-logo.png b/.github/assets/react-sortable-hoc-logo.png new file mode 100644 index 000000000..407fc4a4c Binary files /dev/null and b/.github/assets/react-sortable-hoc-logo.png differ diff --git a/.npmignore b/.npmignore index 93822e5e3..622e97c5b 100644 --- a/.npmignore +++ b/.npmignore @@ -5,11 +5,5 @@ src test .* *.md -server.js -index.html -/index.js -karma.conf.js -webpack.config.*.js -babel.preprocess.sass.js codecov.yml .travis.yml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..5f7b59da0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "arrowParens": "always", + "bracketSpacing": false, + "singleQuote": true, + "trailingComma": "all" +} diff --git a/.storybook/config.js b/.storybook/config.js index 2addbea89..15701fb25 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -1,4 +1,12 @@ -import { configure } from '@kadira/storybook'; +import {addParameters, configure} from '@storybook/react'; +import theme from './theme'; + +addParameters({ + options: { + showAddonPanel: false, + theme, + }, +}); function loadStories() { require('../src/.stories/index.js'); diff --git a/.storybook/head.html b/.storybook/head.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/.storybook/manager-head.html b/.storybook/manager-head.html new file mode 100644 index 000000000..21b3b39c6 --- /dev/null +++ b/.storybook/manager-head.html @@ -0,0 +1,26 @@ + + + + + diff --git a/.storybook/theme.js b/.storybook/theme.js new file mode 100644 index 000000000..3c428b709 --- /dev/null +++ b/.storybook/theme.js @@ -0,0 +1,37 @@ +import {create} from '@storybook/theming'; + +export default create({ + base: 'light', + + colorSecondary: '#9276ff', + + // UI + appBg: 'white', + appContentBg: '#f9f9f9', + appBorderColor: 'grey', + appBorderRadius: 4, + + // Typography + fontBase: '"Roboto", Helvetica Neue, Helvetica, Arial, sans-serif', + fontCode: 'monospace', + + // Text colors + textColor: '#364149', + textInverseColor: 'rgba(255,255,255,0.9)', + + // Toolbar default and active colors + barTextColor: '#FFF', + barSelectedColor: '#FFF', + barBg: '#9276ff', + + // Form colors + inputBg: 'white', + inputBorder: '#efefef', + inputTextColor: '#364149', + inputBorderRadius: 0, + + brandTitle: 'React Sortable HOC', + brandUrl: 'https://github.com/clauderic/react-sortable-hoc', + brandImage: + 'https://user-images.githubusercontent.com/1416436/54170652-dfd59d80-444d-11e9-9c51-658638c0454b.png', +}); diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index 8dffa3c13..b95a82493 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -1,27 +1,30 @@ -var autoprefixer = require('autoprefixer'); -var webpack = require('webpack'); -var path = require('path'); - module.exports = { - plugins: [ - new webpack.NormalModuleReplacementPlugin(/^\.\/layout$/, 'custom-layout') - ], - resolve: { - alias: { - 'custom-layout': path.resolve('.storybook/layout/index.js') - } - }, - module: { - loaders: [ - { - test: /(\.scss)$/, - loaders: ['style', 'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]!postcss!sass?sourceMap'] + module: { + rules: [ + { + test: /(\.scss)$/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + modules: true, + localIdentName: '[name]__[local]', + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: [require('autoprefixer')], }, - { - test: /(\.css)$/, - loaders: ['style', 'css'] - } - ] - }, - postcss: [autoprefixer] -} + }, + 'sass-loader', + ], + }, + { + test: /(\.css)$/, + use: ['style-loader', 'css-loader'], + }, + ], + }, +}; diff --git a/.travis.yml b/.travis.yml index 439013169..5ae48949f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: node_js node_js: - - 5 + - 10 diff --git a/CHANGELOG.md b/CHANGELOG.md index dc9bfd20b..6ecec121e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,97 +1,411 @@ -Changelog ------------- -### 0.6.8 +# Change Log + +All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + + + +# [2.0.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.11.0...v2.0.0) (2021-03-18) + +### Bug Fixes + +- Fixed React strict mode warnings by upgrading to new React context API and removing legacy refs ([#624](https://github.com/clauderic/react-sortable-hoc/pull/624)). Since the new context API was introduced in React ^16.3.0, the peer dependencies had to be upgraded accordingly. +- Check if event is cancellable before calling `event.preventDefault()` [#752](https://github.com/clauderic/react-sortable-hoc/pull/752). +- Fix touch events being lost by listening to event.target on mobile [#586](https://github.com/clauderic/react-sortable-hoc/pull/586). +- Added `disableAutoscroll` prop to PropType definitions [#755](https://github.com/clauderic/react-sortable-hoc/pull/755). + +### Dependencies + +- Updated minimum peer dependencies for `react` and `react-dom` to ^16.3.0. Added ^17.0.0 to list of supported peer dependencies. + + + +# [1.11.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.10.1...v1.11.0) (2020-01-20) + +### Bug Fixes + +- clear autoscroller when autoscroll is disabled ([#604](https://github.com/clauderic/react-sortable-hoc/issues/604)) ([3fd83f9](https://github.com/clauderic/react-sortable-hoc/commit/3fd83f9)) +- Fix UMD path ([#611](https://github.com/clauderic/react-sortable-hoc/pull/611)) ([f61331d](https://github.com/clauderic/react-sortable-hoc/commit/f61331d)) + +### Features + +- Add CSS Grid grid-gap support ([#657](https://github.com/clauderic/react-sortable-hoc/issues/657)) ([4efcaa2](https://github.com/clauderic/react-sortable-hoc/commit/4efcaa2)) + + + +## [1.10.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.10.0...v1.10.1) (2019-08-22) + +### Bug Fixes + +- PropType definition for keyCodes was incorrect ([eaf5070](https://github.com/clauderic/react-sortable-hoc/commit/eaf5070)) + + + +# [1.10.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.9.1...v1.10.0) (2019-08-22) + +### Bug Fixes + +- don't spread the keysToOmit parameter in omit util ([#563](https://github.com/clauderic/react-sortable-hoc/issues/563)) ([1c69772](https://github.com/clauderic/react-sortable-hoc/commit/1c69772)) +- Fix broken UMD build ([#587](https://github.com/clauderic/react-sortable-hoc/issues/587)) ([6cb7750](https://github.com/clauderic/react-sortable-hoc/commit/6cb7750)) +- remove browser field with umd bundle ([#541](https://github.com/clauderic/react-sortable-hoc/issues/541)) ([d3b30fd](https://github.com/clauderic/react-sortable-hoc/commit/d3b30fd) + +### Features + +- Add keyCodes prop to configure the keyboard shortcuts ([#588](https://github.com/clauderic/react-sortable-hoc/issues/588)) ([4c6d8dd](https://github.com/clauderic/react-sortable-hoc/commit/4c6d8dd)) + + + +## [1.9.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.9.0...v1.9.1) (2019-04-24) + +### Bug Fixes + +- do not copy canvas context if it has neither width nor height ([#530](https://github.com/clauderic/react-sortable-hoc/issues/530)) ([3808437](https://github.com/clauderic/react-sortable-hoc/commit/3808437)) +- pass isKeySorting to onSortOver and updateBeforeSortStart handler props ([#531](https://github.com/clauderic/react-sortable-hoc/issues/531)) ([763fd33](https://github.com/clauderic/react-sortable-hoc/commit/763fd33) + + + +# [1.9.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.8.3...v1.9.0) (2019-04-23) + +### Bug Fixes + +- issue with radio input name collision when cloning helper ([5337c97](https://github.com/clauderic/react-sortable-hoc/commit/5337c97)) + +### Features + +- add support for keyboard sorting ([#501](https://github.com/clauderic/react-sortable-hoc/issues/501)) ([439b92f](https://github.com/clauderic/react-sortable-hoc/commit/439b92f)) +- prevent sort start on contentEditable target ([d64c8cf](https://github.com/clauderic/react-sortable-hoc/commit/d64c8cf)) + + + +## [1.8.3](https://github.com/clauderic/react-sortable-hoc/compare/v1.8.2...v1.8.3) (2019-03-20) + +### Bug Fixes + +- issue with windowAsScrollContainer and translation offsets ([0391e62](https://github.com/clauderic/react-sortable-hoc/commit/0391e62)) + +### Features + +- Add disableAutoscroll prop ([#484](https://github.com/clauderic/react-sortable-hoc/issues/484)) ([7845e76](https://github.com/clauderic/react-sortable-hoc/commit/7845e76)) +- added helperContainer prop ([286eff4](https://github.com/clauderic/react-sortable-hoc/commit/286eff4)) +- allow helperContainer prop to be a function returning an HTMLElement ([#489](https://github.com/clauderic/react-sortable-hoc/issues/489)) ([f4a9b4a](https://github.com/clauderic/react-sortable-hoc/commit/f4a9b4a)) +- Detect scroll container automatically ([#507](https://github.com/clauderic/react-sortable-hoc/issues/507)) ([6572921](https://github.com/clauderic/react-sortable-hoc/commit/6572921)) + + + +## [1.8.2](https://github.com/clauderic/react-sortable-hoc/compare/v1.8.1...v1.8.2) (2019-03-19) + +### Bug Fixes + +- issue with getComputedStyle and getScrollingParent ([b104249](https://github.com/clauderic/react-sortable-hoc/commit/b104249)) + + + +## [1.8.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.8.0...v1.8.1) (2019-03-18) + +### Bug Fixes + +- issue with cloning canvas context of dragged items ([#512](https://github.com/clauderic/react-sortable-hoc/issues/512)) ([4df34ad](https://github.com/clauderic/react-sortable-hoc/commit/4df34ad)) + + + +# [1.8.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.8.0) (2019-03-18) + +### Bug Fixes + +- added prop-types to peerDependencies ([0e855c5](https://github.com/clauderic/react-sortable-hoc/commit/0e855c5)) +- copy canvas content into cloned node ([43ad122](https://github.com/clauderic/react-sortable-hoc/commit/43ad122)) +- get updated index after updateBeforeSortStart ([4471a0a](https://github.com/clauderic/react-sortable-hoc/commit/4471a0a)) +- helperContainer PropType definition broke server-side rendering ([#471](https://github.com/clauderic/react-sortable-hoc/issues/471)) ([c0eef97](https://github.com/clauderic/react-sortable-hoc/commit/c0eef97)) +- lock axis story should not use lockToContainerEdges ([db1d3a9](https://github.com/clauderic/react-sortable-hoc/commit/db1d3a9)) +- omit disableAutoscroll prop ([#502](https://github.com/clauderic/react-sortable-hoc/issues/502)) ([e994e73](https://github.com/clauderic/react-sortable-hoc/commit/e994e73)) +- omit spreading helperContainer prop ([#497](https://github.com/clauderic/react-sortable-hoc/issues/497)) ([12bafdf](https://github.com/clauderic/react-sortable-hoc/commit/12bafdf)) +- overflow bug while dragging an item upwards in a grid ([1a2c87e](https://github.com/clauderic/react-sortable-hoc/commit/1a2c87e)) +- replace process.env.NODE_ENV in UMD builds ([16135df](https://github.com/clauderic/react-sortable-hoc/commit/16135df)) +- update helperContainer prop type definition ([#491](https://github.com/clauderic/react-sortable-hoc/issues/491)) ([fd30383](https://github.com/clauderic/react-sortable-hoc/commit/fd30383)) +- updated the behaviour of disabled elements ([bd3d041](https://github.com/clauderic/react-sortable-hoc/commit/bd3d041)) +- virtualized collection grid bug ([a57975c](https://github.com/clauderic/react-sortable-hoc/commit/a57975c)) + +### Features + +- Add disableAutoscroll prop ([#484](https://github.com/clauderic/react-sortable-hoc/issues/484)) ([7845e76](https://github.com/clauderic/react-sortable-hoc/commit/7845e76)) +- added helperContainer prop ([286eff4](https://github.com/clauderic/react-sortable-hoc/commit/286eff4)) +- allow helperContainer prop to be a function returning an HTMLElement ([#489](https://github.com/clauderic/react-sortable-hoc/issues/489)) ([f4a9b4a](https://github.com/clauderic/react-sortable-hoc/commit/f4a9b4a)) +- Detect scroll container automatically ([#507](https://github.com/clauderic/react-sortable-hoc/issues/507)) ([6572921](https://github.com/clauderic/react-sortable-hoc/commit/6572921)) + + + +## [1.7.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.7.1) (2019-03-06) + +### Bug Fixes + +- updated the behaviour of disabled elements ([bd3d041](https://github.com/clauderic/react-sortable-hoc/commit/bd3d041)) + + + +# [1.7.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.7.0) (2019-03-06) + +### Bug Fixes + +- updated the behaviour of disabled elements ([bd3d041](https://github.com/clauderic/react-sortable-hoc/commit/bd3d041)) + + + +## [1.6.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.6.0...v1.6.1) (2019-02-11) + +### Bug Fixes + +- omit disableAutoscroll prop ([#502](https://github.com/clauderic/react-sortable-hoc/issues/502)) ([e994e73](https://github.com/clauderic/react-sortable-hoc/commit/e994e73)) + + + +# [1.6.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.5.3...v1.6.0) (2019-02-07) + +### Features + +- Add disableAutoscroll prop ([#484](https://github.com/clauderic/react-sortable-hoc/issues/484)) ([7845e76](https://github.com/clauderic/react-sortable-hoc/commit/7845e76)) + + + +## [1.5.4](https://github.com/clauderic/react-sortable-hoc/compare/v1.5.3...v1.5.4) (2019-02-07) + +### Bug Fixes + +- overflow bug while dragging an item upwards in a grid ([1a2c87e](https://github.com/clauderic/react-sortable-hoc/commit/1a2c87e)) +- virtualized collection grid bug ([a57975c](https://github.com/clauderic/react-sortable-hoc/commit/a57975c)) + + + +## [1.5.3](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.5.3) (2019-01-25) + +### Bug Fixes + +- omit spreading helperContainer prop on WrappedComponent ([#497](https://github.com/clauderic/react-sortable-hoc/issues/497)) ([12bafdf](https://github.com/clauderic/react-sortable-hoc/commit/12bafdf)) + + + +## [1.5.2](https://github.com/clauderic/react-sortable-hoc/compare/v1.5.1...v1.5.2) (2019-01-22) + +### Bug Fixes + +- invalid helperContainer PropType definition ([#493](https://github.com/clauderic/react-sortable-hoc/issues/493)) ([dc1d18f](https://github.com/clauderic/react-sortable-hoc/commit/dc1d18f)) + + + +## [1.5.1](https://github.com/clauderic/react-sortable-hoc/compare/v1.5.0...v1.5.1) (2019-01-22) + +### Bug Fixes + +- update helperContainer prop type definition ([#491](https://github.com/clauderic/react-sortable-hoc/issues/491)) ([fd30383](https://github.com/clauderic/react-sortable-hoc/commit/fd30383)) + + + +# [1.5.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.4.0...v1.5.0) (2019-01-22) + +### Features + +- allow helperContainer prop to be a function returning an HTMLElement ([f4a9b4a](https://github.com/clauderic/react-sortable-hoc/commit/f4a9b4a)) + + + +# [1.4.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.3.0...v1.4.0) (2019-01-10) + +### Bug Fixes + +- Fix CommonJS and UMD builds by using Rollup and Babel to generate the bundles ([#474](https://github.com/clauderic/react-sortable-hoc/issues/474)) + + + +# [1.3.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.2.0...v1.3.0) (2019-01-08) + +### Bug Fixes + +- helperContainer PropType definition broke server-side rendering ([#471](https://github.com/clauderic/react-sortable-hoc/issues/471)) ([c0eef97](https://github.com/clauderic/react-sortable-hoc/commit/c0eef97)) + + + +# [1.2.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.1.0...v1.2.0) (2019-01-08) + +### Features + +- added helperContainer prop ([286eff4](https://github.com/clauderic/react-sortable-hoc/commit/286eff4)) + + + +# [1.1.0](https://github.com/clauderic/react-sortable-hoc/compare/v1.0.0...v1.1.0) (2019-01-07) + +### Features + +- added updateBeforeSortStart prop ([162857b](https://github.com/clauderic/react-sortable-hoc/commit/162857b)) + + + +# [1.0.0](https://github.com/clauderic/react-sortable-hoc/compare/v0.8.4...v1.0.0) (2019-01-07) + +### BREAKING CHANGES + +- The UMD release no longer includes babel-polyfill, you will need to include your own polyfills in order to support older browsers. + +# 0.8.4 + +- Fix a bug when you use SortableHandle and distance prop [#447](https://github.com/clauderic/react-sortable-hoc/pull/447) + +# 0.8.3 + +- Fix: TouchEvent is undefined in certain browsers, such as Safari [#382](https://github.com/clauderic/react-sortable-hoc/issues/382) + +# 0.8.1 + +- Fix scrolling issues on mobile with anchor tag elements [#380](https://github.com/clauderic/react-sortable-hoc/pull/380) +- Update TypeScript type definition for ContainerGetter to accept Promises that return HTMLElements + +# 0.8.0 + +- Allow `getContainer` to return a promise. This is useful when the container node is rendered by a parent component, since `componentDidMount` fires backwards (from child to parent) [#155](https://github.com/clauderic/react-sortable-hoc/pull/155/) + +# 0.7.4 + +- Fix typo in getLockPixelOffset helper + +# 0.7.3 + +- Fix issues with distance and pressThreshold props on mobile [#378](https://github.com/clauderic/react-sortable-hoc/pull/378) + +# 0.7.2 + +- Fix issues with TypeScript type definitions + +# 0.7.1 + +- Provide TypeScript type definitions out of the box [#377](https://github.com/clauderic/react-sortable-hoc/pull/377) +- Fix potential issues with calling `removeEventListeners` on `componentWillUnmount` if the container node has already unmounted [#376](https://github.com/clauderic/react-sortable-hoc/pull/376) + +# 0.7.0 + +- [Breaking change] Removed lodash dependency. For users wishing to support Internet Explorer, a [polyfill](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find#Polyfill) for Array.prototype.find will be required +- Added `onSortOver` prop that gets invoked when sorting over an element [#278](https://github.com/clauderic/react-sortable-hoc/pull/278) +- Fix `useWindowAsScrollContainer` [#306](https://github.com/clauderic/react-sortable-hoc/pull/306) + +# 0.6.8 + Update react and react-dom peerdependency requirements for React 16+ [#283](https://github.com/clauderic/react-sortable-hoc/pull/283). Thanks [@jnsdls](https://github.com/jnsdls)! -### 0.6.7 +# 0.6.7 + Fixes issues with Jest Snapshot testing trying to serialize the `window` object and running out of memory [#249](https://github.com/clauderic/react-sortable-hoc/issues/249). Thanks [@cameronmcefee](https://github.com/cameronmcefee)! -### 0.6.6 +# 0.6.6 + Fixes an issue with Internet Explorer 11 introduced in `0.6.5` [#248](https://github.com/clauderic/react-sortable-hoc/pull/248). Thanks [@humiston](https://github.com/humiston)! -### 0.6.5 +# 0.6.5 + Fixes the position of the sortable helper when the page is scrolled [#213](https://github.com/clauderic/react-sortable-hoc/pull/213) -### 0.6.4 +# 0.6.4 + Fix: when cloning the element that is being sorted, we no longer update the value of cloned file inputs [#232](https://github.com/clauderic/react-sortable-hoc/pull/232) -### 0.6.3 +# 0.6.3 + Fixes issues caused by a disabled SortableElement being moved when `distance` is set to a value other than `0` -### 0.6.2 +# 0.6.2 + Use `prop-types` package for PropType validation for compatibility with React ^15.5 -### 0.6.1 +# 0.6.1 + Tweak: default to `pointerEvents: none` on sortable helper, this way the underlying view can still be scrolled using the trackpad/mousewheel while sorting [#160](https://github.com/clauderic/react-sortable-hoc/pull/160) -### 0.6.0 +# 0.6.0 + Feature: added `pressThreshold` prop to make `pressDelay` fault tolerant [#159](https://github.com/clauderic/react-sortable-hoc/pull/159) -### 0.5.0 +# 0.5.0 + Tweak: `button` elements are now included in the default `shouldCancelStart` implementation [#142](https://github.com/clauderic/react-sortable-hoc/pull/142). Fix: Omit `getHelperDimensions` before passing down props in `SortableContainer` -### 0.4.12 +# 0.4.12 + Fix: This release fixes some issues caused by the `onSortEnd` callback being invoked before `setState` [#82](https://github.com/clauderic/react-sortable-hoc/issues/82). -### 0.4.10 -Fix: This version fixes issues with nested `SortableContainer` elements using drag handles from also dragging their parent [#112](https://github.com/clauderic/react-sortable-hoc/issues/112), #127(https://github.com/clauderic/react-sortable-hoc/pull/127). Thanks []@DeadHeadRussell](https://github.com/DeadHeadRussell)! +# 0.4.10 + +Fix: This version fixes issues with nested `SortableContainer` elements using drag handles from also dragging their parent [#112](https://github.com/clauderic/react-sortable-hoc/issues/112), #127(https://github.com/clauderic/react-sortable-hoc/pull/127). Thanks [@DeadHeadRussell](https://github.com/DeadHeadRussell)! + +# 0.4.9 -### 0.4.9 Fix: This release fixes a bug introduced in `0.4.8` caused by calling the `forEach` method directly on a NodeList, which is undefined in a number of browsers [#125](https://github.com/clauderic/react-sortable-hoc/issues/125) -### 0.4.8 +# 0.4.8 + Fix: Added logic to ensure that `select`, `input` and `textarea` fields in `SortableElement` always retain their `value` when the element is cloned (this happens when sorting begins) [#122](https://github.com/clauderic/react-sortable-hoc/issues/122) [#123](https://github.com/clauderic/react-sortable-hoc/pull/123). Thanks [@tomasztomys](https://github.com/tomasztomys)! -### 0.4.7 +# 0.4.7 + Fix: This release fixes a bug in Firefox caused by active anchor tags preventing mousemove events from being fired [#118](https://github.com/clauderic/react-sortable-hoc/issues/118) -### 0.4.5 +# 0.4.5 + Fix: getHelperDimensions height was not being used (Thanks [@SMenigat](https://github.com/SMenigat)!) -### 0.4.4 +# 0.4.4 + Tweak: cherry-picking lodash methods instead of importing the entire bundle (slipped by in a PR, thanks for pointing this out [@arackaf](https://github.com/arackaf)!) -### 0.4.3 +# 0.4.3 + Fixes an edge-case bug in Firefox where window.getComputedStyle() returns null inside an iframe with `display: none` [#106](https://github.com/clauderic/react-sortable-hoc/pull/106). Thanks [@funnel-mark](https://github.com/funnel-mark)! -### 0.4.2 +# 0.4.2 + Fixes an issue when attempting to sort items while rapidly moving the mouse. By setting an immediate timer, we move the cancel event to the tail of the timer queue, and ensure that it is fired after the pressTimer [#80](https://github.com/clauderic/react-sortable-hoc/pull/80). Thanks [@v0lkan](https://github.com/v0lkan)! -### 0.4.0 -– Fix a timing issue in Chrome caused by setTimeout [#71](https://github.com/clauderic/react-sortable-hoc/pull/71) -– Private props are no longer passed down to the wrapped component [#98](https://github.com/clauderic/react-sortable-hoc/pull/98) +# 0.4.0 + +- Fix a timing issue in Chrome caused by setTimeout [#71](https://github.com/clauderic/react-sortable-hoc/pull/71) +- Private props are no longer passed down to the wrapped component [#98](https://github.com/clauderic/react-sortable-hoc/pull/98) + +# 0.3.0 -### 0.3.0 Added grid support for elements of equal widths / heights [#4](https://github.com/clauderic/react-sortable-hoc/issues/4) [#86](https://github.com/clauderic/react-sortable-hoc/pull/86). Huge shout-out to [@richmeij](https://github.com/richmeij) for making this happen! -### 0.2.0 +# 0.2.0 + Add a `getHelperDimensions` prop to control SortableHelper size [#83](https://github.com/clauderic/react-sortable-hoc/issues/83). Thanks [@nervetattoo](https://github.com/nervetattoo)! -### 0.1.1 +# 0.1.1 + Added `touchCancel` listener to properly handle canceled touches [#73](https://github.com/clauderic/react-sortable-hoc/pull/73) -### 0.1.0 +# 0.1.0 + - Force `box-sizing: border-box` on sortable helper [#67](https://github.com/clauderic/react-sortable-hoc/issues/67) - Support changing an item's collection prop on the fly [#66](https://github.com/clauderic/react-sortable-hoc/pull/66) -### 0.0.11 +# 0.0.11 + Utilize babel-plugin-transform-runtime to utilize `babelHelpers` without them being required in application code [#45](https://github.com/clauderic/react-sortable-hoc/issues/45) -### 0.0.10 +# 0.0.10 + The `arrayMove` helper no longer mutates the array, it now returns a new array [#61](https://github.com/clauderic/react-sortable-hoc/issues/61) -### 0.0.9 +# 0.0.9 + Server-side rendering bugfix: safeguard against `document` being undefined [#59](https://github.com/clauderic/react-sortable-hoc/pull/59) -### 0.0.8 +# 0.0.8 + - Added `distance` prop ([#35](https://github.com/clauderic/react-sortable-hoc/issues/35)) - Added a `shouldCancelStart` ([#47](https://github.com/clauderic/react-sortable-hoc/issues/47), [#36](https://github.com/clauderic/react-sortable-hoc/issues/36), [#41](https://github.com/clauderic/react-sortable-hoc/issues/41)) prop to programatically cancel sorting before it begins. - Prevent right click from causing sort start ([#46](https://github.com/clauderic/react-sortable-hoc/issues/46)) -### 0.0.7 +# 0.0.7 + Fixes server-side rendering (window undefined) ([#39](https://github.com/clauderic/react-sortable-hoc/issues/39)) -### 0.0.6 +# 0.0.6 + - Added support for a custom container ([#37](https://github.com/clauderic/react-sortable-hoc/issues/37)) - Fix changing disable property while receiving props ([#34](https://github.com/clauderic/react-sortable-hoc/issues/34)) diff --git a/README.md b/README.md index 0921dbb21..a5ad3d8e1 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,39 @@ -# React Sortable (HOC) -> A set of higher-order components to turn any list into an animated, touch-friendly, sortable list. +> **Warning** +> +> This library is **no longer actively maintained**. It will continue to receive critical security updates, but there are no new features planned. +> In future versions of React, the [`findDOMNode`](https://reactjs.org/docs/react-dom.html#finddomnode) method will be deprecated. This method is a critical piece of the architecture of `react-sortable-hoc`, and the library will stop working in the future when that method is removed from `react-dom`. +> +> All development efforts have been redirected towards [**@dnd-kit**](https://github.com/clauderic/dnd-kit). It provides feature parity, built with a modern and extensible architecture, supports complex use-cases and has accessibility features built-in. New consumers are strongly encouraged to adopt **@dnd-kit** instead of adopting `react-sortable-hoc`. + +Visit @dnd-kit github repository + +# React Sortable HOC + +> A set of higher-order components to turn any list into an animated, accessible and touch-friendly sortable list [![npm version](https://img.shields.io/npm/v/react-sortable-hoc.svg)](https://www.npmjs.com/package/react-sortable-hoc) [![npm downloads](https://img.shields.io/npm/dm/react-sortable-hoc.svg)](https://www.npmjs.com/package/react-sortable-hoc) [![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/clauderic/react-sortable-hoc/blob/master/LICENSE) [![Gitter](https://badges.gitter.im/clauderic/react-sortable-hoc.svg)](https://gitter.im/clauderic/react-sortable-hoc) -![gzip size](http://img.badgesize.io/https://npmcdn.com/react-sortable-hoc/dist/umd/react-sortable-hoc.min.js?compression=gzip) +![gzip size](http://img.badgesize.io/https://npmcdn.com/react-sortable-hoc/dist/react-sortable-hoc.umd.min.js?compression=gzip) ### Examples available here: http://clauderic.github.io/react-sortable-hoc/ -Features ---------------- -* **Higher Order Components** – Integrates with your existing components -* **Drag handle, auto-scrolling, locked axis, events, and more!** -* **Suuuper smooth animations** – Chasing the 60FPS dream 🌈 -* **Works with virtualization libraries: [react-virtualized](https://github.com/bvaughn/react-virtualized/), [react-tiny-virtual-list](https://github.com/clauderic/react-tiny-virtual-list), [react-infinite](https://github.com/seatgeek/react-infinite), etc.** -* **Horizontal lists, vertical lists, or a grid** ↔ ↕ ⤡ -* **Touch support** 👌 +## Features -Installation ------------- +- **Higher Order Components** – Integrates with your existing components +- **Drag handle, auto-scrolling, locked axis, events, and more!** +- **Suuuper smooth animations** – Chasing the 60FPS dream 🌈 +- **Works with virtualization libraries: [react-virtualized](https://github.com/bvaughn/react-virtualized/), [react-tiny-virtual-list](https://github.com/clauderic/react-tiny-virtual-list), [react-infinite](https://github.com/seatgeek/react-infinite), etc.** +- **Horizontal lists, vertical lists, or a grid** ↔ ↕ ⤡ +- **Touch support** 👌 +- **Accessible: supports keyboard sorting** -Using [npm](https://www.npmjs.com/package/react-sortable-hoc): +## Installation - $ npm install react-sortable-hoc --save +Using [npm](https://www.npmjs.com/package/react-sortable-hoc): + $ npm install react-sortable-hoc --save Then, using a module bundler that supports either CommonJS or ES2015 modules, such as [webpack](https://github.com/webpack/webpack): @@ -39,28 +48,28 @@ var SortableElement = Sortable.SortableElement; ``` Alternatively, an UMD build is also available: + ```html - + ``` -Usage ------------- +## Usage + ### Basic Example ```js import React, {Component} from 'react'; import {render} from 'react-dom'; -import {SortableContainer, SortableElement, arrayMove} from 'react-sortable-hoc'; +import {SortableContainer, SortableElement} from 'react-sortable-hoc'; +import arrayMove from 'array-move'; -const SortableItem = SortableElement(({value}) => -
  • {value}
  • -); +const SortableItem = SortableElement(({value}) =>
  • {value}
  • ); const SortableList = SortableContainer(({items}) => { return ( ); @@ -71,96 +80,117 @@ class SortableComponent extends Component { items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'], }; onSortEnd = ({oldIndex, newIndex}) => { - this.setState({ - items: arrayMove(this.state.items, oldIndex, newIndex), - }); + this.setState(({items}) => ({ + items: arrayMove(items, oldIndex, newIndex), + })); }; render() { return ; } } -render(, document.getElementById('root')); +render(, document.getElementById('root')); ``` + That's it! React Sortable does not come with any styles by default, since it's meant to enhance your existing components. More code examples are available [here](https://github.com/clauderic/react-sortable-hoc/blob/master/examples/). -Why should I use this? --------------------- +## Why should I use this? + There are already a number of great Drag & Drop libraries out there (for instance, [react-dnd](https://github.com/gaearon/react-dnd/) is fantastic). If those libraries fit your needs, you should definitely give them a try first. However, most of those libraries rely on the HTML5 Drag & Drop API, which has some severe limitations. For instance, things rapidly become tricky if you need to support touch devices, if you need to lock dragging to an axis, or want to animate the nodes as they're being sorted. React Sortable HOC aims to provide a simple set of higher-order components to fill those gaps. If you're looking for a dead-simple, mobile-friendly way to add sortable functionality to your lists, then you're in the right place. ### Prop Types #### SortableContainer HOC -| Property | Type | Default | Description | -|:---------------------------|:------------------|:-----------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| axis | String | `y` | Items can be sorted horizontally, vertically or in a grid. Possible values: `x`, `y` or `xy` | -| lockAxis | String | | If you'd like, you can lock movement to an axis while sorting. This is not something that is possible with HTML5 Drag & Drop | -| helperClass | String | | You can provide a class you'd like to add to the sortable helper to add some styles to it | -| transitionDuration | Number | `300` | The duration of the transition when elements shift positions. Set this to `0` if you'd like to disable transitions | -| pressDelay | Number | `0` | If you'd like elements to only become sortable after being pressed for a certain time, change this property. A good sensible default value for mobile is `200`. Cannot be used in conjunction with the `distance` prop. | -| pressThreshold | Number | `5` | Number of pixels of movement to tolerate before ignoring a press event. | -| distance | Number | `0` | If you'd like elements to only become sortable after being dragged a certain number of pixels. Cannot be used in conjunction with the `pressDelay` prop. | -| shouldCancelStart | Function | [Function](https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableContainer/index.js#L48) | This function is invoked before sorting begins, and can be used to programatically cancel sorting before it begins. By default, it will cancel sorting if the event target is either an `input`, `textarea`, `select` or `option`. | -| onSortStart | Function | | Callback that is invoked when sorting begins. `function({node, index, collection}, event)` | -| onSortMove | Function | | Callback that is invoked during sorting as the cursor moves. `function(event)` | -| onSortEnd | Function | | Callback that is invoked when sorting ends. `function({oldIndex, newIndex, collection}, e)` | -| useDragHandle | Boolean | `false` | If you're using the `SortableHandle` HOC, set this to `true` | -| useWindowAsScrollContainer | Boolean | `false` | If you want, you can set the `window` as the scrolling container | -| hideSortableGhost | Boolean | `true` | Whether to auto-hide the ghost element. By default, as a convenience, React Sortable List will automatically hide the element that is currently being sorted. Set this to false if you would like to apply your own styling. | -| lockToContainerEdges | Boolean | `false` | You can lock movement of the sortable element to it's parent `SortableContainer` | -| lockOffset | `OffsetValue`\* \ | [`OffsetValue`\*, `OffsetValue`\*] | `"50%"` | When `lockToContainerEdges` is set to `true`, this controls the offset distance between the sortable helper and the top/bottom edges of it's parent `SortableContainer`. Percentage values are relative to the height of the item currently being sorted. If you wish to specify different behaviours for locking to the *top* of the container vs the *bottom*, you may also pass in an `array` (For example: `["0%", "100%"]`). | -| getContainer | Function | | Optional function to return the scrollable container element. This property defaults to the `SortableContainer` element itself or (if `useWindowAsScrollContainer` is true) the window. Use this function to specify a custom container object (eg this is useful for integrating with certain 3rd party components such as `FlexTable`). This function is passed a single parameter (the `wrappedInstance` React element) and it is expected to return a DOM element. | -| getHelperDimensions | Function | [Function](https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableContainer/index.js#L58) | Optional `function({node, index, collection})` that should return the computed dimensions of the SortableHelper. See [default implementation](https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableContainer/index.js#L58) for more details | + +| Property | Type | Default | Description | +| :-------------------------------- | :-------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| axis | String | `y` | Items can be sorted horizontally, vertically or in a grid. Possible values: `x`, `y` or `xy` | +| lockAxis | String | | If you'd like, you can lock movement to an axis while sorting. This is not something that is possible with HTML5 Drag & Drop. Possible values: `x` or `y`. | +| helperClass | String | | You can provide a class you'd like to add to the sortable helper to add some styles to it | +| transitionDuration | Number | `300` | The duration of the transition when elements shift positions. Set this to `0` if you'd like to disable transitions | +| keyboardSortingTransitionDuration | Number | `transitionDuration` | The duration of the transition when the helper is shifted during keyboard sorting. Set this to `0` if you'd like to disable transitions for the keyboard sorting helper. Defaults to the value set for `transitionDuration` if undefined | +| keyCodes | Array | `{`
      `lift: [32],`
      `drop: [32],`
      `cancel: [27],`
      `up: [38, 37],`
      `down: [40, 39]`
    `}` | An object containing an array of keycodes for each keyboard-accessible action. | +| pressDelay | Number | `0` | If you'd like elements to only become sortable after being pressed for a certain time, change this property. A good sensible default value for mobile is `200`. Cannot be used in conjunction with the `distance` prop. | +| pressThreshold | Number | `5` | Number of pixels of movement to tolerate before ignoring a press event. | +| distance | Number | `0` | If you'd like elements to only become sortable after being dragged a certain number of pixels. Cannot be used in conjunction with the `pressDelay` prop. | +| shouldCancelStart | Function | [Function](https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableContainer/index.js#L48) | This function is invoked before sorting begins, and can be used to programatically cancel sorting before it begins. By default, it will cancel sorting if the event target is either an `input`, `textarea`, `select`, `option`, or `button`. | +| updateBeforeSortStart | Function | | This function is invoked before sorting begins. It can return a promise, allowing you to run asynchronous updates (such as `setState`) before sorting begins. `function({node, index, collection, isKeySorting}, event)` | +| onSortStart | Function | | Callback that is invoked when sorting begins. `function({node, index, collection, isKeySorting}, event)` | +| onSortMove | Function | | Callback that is invoked during sorting as the cursor moves. `function(event)` | +| onSortOver | Function | | Callback that is invoked when moving over an item. `function({index, oldIndex, newIndex, collection, isKeySorting}, e)` | +| onSortEnd | Function | | Callback that is invoked when sorting ends. `function({oldIndex, newIndex, collection, isKeySorting}, e)` | +| useDragHandle | Boolean | `false` | If you're using the `SortableHandle` HOC, set this to `true` | +| useWindowAsScrollContainer | Boolean | `false` | If you want, you can set the `window` as the scrolling container | +| hideSortableGhost | Boolean | `true` | Whether to auto-hide the ghost element. By default, as a convenience, React Sortable List will automatically hide the element that is currently being sorted. Set this to false if you would like to apply your own styling. | +| lockToContainerEdges | Boolean | `false` | You can lock movement of the sortable element to it's parent `SortableContainer` | +| lockOffset | `OffsetValue`\* | [`OffsetValue`\*, `OffsetValue`\*] | `"50%"` | When`lockToContainerEdges`is set to`true`, this controls the offset distance between the sortable helper and the top/bottom edges of it's parent`SortableContainer`. Percentage values are relative to the height of the item currently being sorted. If you wish to specify different behaviours for locking to the _top_ of the container vs the _bottom_, you may also pass in an`array`(For example:`["0%", "100%"]`). | +| getContainer | Function | | Optional function to return the scrollable container element. This property defaults to the `SortableContainer` element itself or (if `useWindowAsScrollContainer` is true) the window. Use this function to specify a custom container object (eg this is useful for integrating with certain 3rd party components such as `FlexTable`). This function is passed a single parameter (the `wrappedInstance` React element) and it is expected to return a DOM element. | +| getHelperDimensions | Function | [Function](https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableContainer/index.js#L74-L77) | Optional `function({node, index, collection})` that should return the computed dimensions of the SortableHelper. See [default implementation](https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableContainer/defaultGetHelperDimensions.js) for more details | +| helperContainer | HTMLElement | Function | `document.body` | By default, the cloned sortable helper is appended to the document body. Use this prop to specify a different container for the sortable clone to be appended to. Accepts an `HTMLElement` or a function returning an `HTMLElement` that will be invoked before right before sorting begins | +| disableAutoscroll | Boolean | `false` | Disables autoscrolling while dragging | \* `OffsetValue` can either be a finite `Number` or a `String` made up of a number and a unit (`px` or `%`). Examples: `10` (which is the same as `"10px"`), `"50%"` #### SortableElement HOC + | Property | Type | Default | Required? | Description | -|:-----------|:-----------------|:--------|:---------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| :--------- | :--------------- | :------ | :-------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | index | Number | | ✓ | This is the element's sortableIndex within it's collection. This prop is required. | | collection | Number or String | `0` | | The collection the element is part of. This is useful if you have multiple groups of sortable elements within the same `SortableContainer`. [Example](http://clauderic.github.io/react-sortable-hoc/#/basic-configuration/multiple-lists) | | disabled | Boolean | `false` | | Whether the element should be sortable or not | -FAQ ---------------- +## FAQ + ### Running Examples -In root folder: +In root folder, run the following commands to launch React Storybook: ``` - $ npm install - $ npm run storybook +$ npm install +$ npm start ``` +### Accessibility + +React Sortable HOC supports keyboard sorting out of the box. To enable it, make sure your `SortableElement` or `SortableHandle` is focusable. This can be done by setting `tabIndex={0}` on the outermost HTML node rendered by the component you're enhancing with `SortableElement` or `SortableHandle`. + +Once an item is focused/tabbed to, pressing `SPACE` picks it up, `ArrowUp` or `ArrowLeft` moves it one place backward in the list, `ArrowDown` or `ArrowRight` moves items one place forward in the list, pressing `SPACE` again drops the item in its new position. Pressing `ESC` before the item is dropped will cancel the sort operations. + ### Grid support + Need to sort items in a grid? We've got you covered! Just set the `axis` prop to `xy`. Grid support is currently limited to a setup where all the cells in the grid have the same width and height, though we're working hard to get variable width support in the near future. ### Item disappearing when sorting / CSS issues + Upon sorting, `react-sortable-hoc` creates a clone of the element you are sorting (the _sortable-helper_) and appends it to the end of the `` tag. The original element will still be in-place to preserve its position in the DOM until the end of the drag (with inline-styling to make it invisible). If the _sortable-helper_ gets messed up from a CSS standpoint, consider that maybe your selectors to the draggable item are dependent on a parent element which isn't present anymore (again, since the _sortable-helper_ is at the end of the ``). This can also be a `z-index` issue, for example, when using `react-sortable-hoc` within a Bootstrap modal, you'll need to increase the `z-index` of the SortableHelper so it is displayed on top of the modal (see [#87](https://github.com/clauderic/react-sortable-hoc/issues/87) for more details). ### Click events being swallowed + By default, `react-sortable-hoc` is triggered immediately on `mousedown`. If you'd like to prevent this behaviour, there are a number of strategies readily available. You can use the `distance` prop to set a minimum distance (in pixels) to be dragged before sorting is enabled. You can also use the `pressDelay` prop to add a delay before sorting is enabled. Alternatively, you can also use the [SortableHandle](https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableHandle/index.js) HOC. ### Wrapper props not passed down to wrapped Component + All props for `SortableContainer` and `SortableElement` listed above are intentionally consumed by the wrapper component and are **not** passed down to the wrapped component. To make them available pass down the desired prop again with a different name. E.g.: ```js -const SortableItem = SortableElement(({value, sortIndex}) => -
  • {value} - #{sortIndex}
  • -); +const SortableItem = SortableElement(({value, sortIndex}) => ( +
  • + {value} - #{sortIndex} +
  • +)); const SortableList = SortableContainer(({items}) => { return (
      {items.map((value, index) => ( - ))}
    @@ -168,18 +198,20 @@ const SortableList = SortableContainer(({items}) => { }); ``` -Dependencies ------------- -React Sortable List has very few dependencies. It depends on `invariant` and a handful of `lodash` helpers. It has the following peerDependencies: `react`, `react-dom` +## Dependencies + +React Sortable HOC only depends on [invariant](https://github.com/zertosh/invariant). It has the following peerDependencies: `react`, `react-dom` + +## Reporting Issues -Reporting Issues ----------------- -If believe you've found an issue, please [report it](https://github.com/clauderic/react-sortable-hoc/issues) along with any relevant details to reproduce it. The easiest way to do so is to fork this [jsfiddle](https://jsfiddle.net/clauderic/6r7r2cva/). +If believe you've found an issue, please [report it](https://github.com/clauderic/react-sortable-hoc/issues) along with any relevant details to reproduce it. The easiest way to do so is to fork the `react-sortable-hoc` basic setup sandbox on [CodeSandbox](https://codesandbox.io/s/o104x95y86): + +[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-sortable-hoc-starter-o104x95y86) + +## Asking for help -Asking for help ----------------- Please do not use the issue tracker for personal support requests. Instead, use [Gitter](https://gitter.im/clauderic/react-sortable-hoc) or StackOverflow. -Contributions ------------- +## Contributions + Yes please! Feature requests / pull requests are welcome. diff --git a/babel.preprocess.sass.js b/babel.preprocess.sass.js deleted file mode 100644 index 587f37e35..000000000 --- a/babel.preprocess.sass.js +++ /dev/null @@ -1,10 +0,0 @@ -var sass = require('node-sass'); - -module.exports = function processSass(data, filename) { - var result; - result = sass.renderSync({ - data: data, - file: filename - }).css; - return result.toString('utf8'); -}; diff --git a/examples/.eslintrc.json b/examples/.eslintrc.json new file mode 100644 index 000000000..82010f8c4 --- /dev/null +++ b/examples/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "rules": { + "import/no-unresolved": "off", + "react/prop-types": "off", + "react/no-array-index-key": "off" + } +} diff --git a/examples/basic.js b/examples/basic.js index 20730c9a7..8f5e4dfad 100644 --- a/examples/basic.js +++ b/examples/basic.js @@ -1,31 +1,36 @@ import React, {Component} from 'react'; import {render} from 'react-dom'; -import {SortableContainer, SortableElement, arrayMove} from 'react-sortable-hoc'; +import {sortableContainer, sortableElement} from 'react-sortable-hoc'; +import arrayMove from 'array-move'; -const SortableItem = SortableElement(({value}) =>
  • {value}
  • ); +const SortableItem = sortableElement(({value}) =>
  • {value}
  • ); -const SortableList = SortableContainer(({items}) => { - return ( -
      - {items.map((value, index) => ( - - ))} -
    - ); +const SortableContainer = sortableContainer(({children}) => { + return
      {children}
    ; }); -class SortableComponent extends Component { +class App extends Component { state = { items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'], }; + onSortEnd = ({oldIndex, newIndex}) => { - this.setState({ - items: arrayMove(this.state.items, oldIndex, newIndex), - }); + this.setState(({items}) => ({ + items: arrayMove(items, oldIndex, newIndex), + })); }; + render() { - return ; + const {items} = this.state; + + return ( + + {items.map((value, index) => ( + + ))} + + ); } } -render(, document.getElementById('root')); +render(, document.getElementById('root')); diff --git a/examples/collections.js b/examples/collections.js new file mode 100644 index 000000000..75bad8e49 --- /dev/null +++ b/examples/collections.js @@ -0,0 +1,56 @@ +import React, {Component} from 'react'; +import {render} from 'react-dom'; +import {sortableContainer, sortableElement} from 'react-sortable-hoc'; +import arrayMove from 'array-move'; + +const SortableItem = sortableElement(({value}) =>
  • {value}
  • ); + +const SortableContainer = sortableContainer(({children}) => { + return
    {children}
    ; +}); + +class App extends Component { + state = { + collections: [[0, 1, 2], [0, 1, 2, 3, 4], [0, 1, 2]], + }; + + onSortEnd = ({oldIndex, newIndex, collection}) => { + this.setState(({collections}) => { + const newCollections = [...collections]; + + newCollections[collection] = arrayMove( + collections[collection], + oldIndex, + newIndex, + ); + + return {collections: newCollections}; + }); + }; + + render() { + const {collections} = this.state; + + return ( + + {collections.map((items, index) => ( + + LIST {index} +
      + {items.map((item, i) => ( + + ))} +
    +
    + ))} +
    + ); + } +} + +render(, document.getElementById('root')); diff --git a/examples/drag-handle.js b/examples/drag-handle.js index c1c1ef173..35b278000 100644 --- a/examples/drag-handle.js +++ b/examples/drag-handle.js @@ -1,49 +1,47 @@ import React, {Component} from 'react'; import {render} from 'react-dom'; import { - SortableContainer, - SortableElement, - SortableHandle, - arrayMove, + sortableContainer, + sortableElement, + sortableHandle, } from 'react-sortable-hoc'; +import arrayMove from 'array-move'; -const DragHandle = SortableHandle(() => ::); // This can be any component you want +const DragHandle = sortableHandle(() => ::); -const SortableItem = SortableElement(({value}) => { - return ( -
  • - - {value} -
  • - ); -}); +const SortableItem = sortableElement(({value}) => ( +
  • + + {value} +
  • +)); -const SortableList = SortableContainer(({items}) => { - return ( -
      - {items.map((value, index) => ( - - ))} -
    - ); +const SortableContainer = sortableContainer(({children}) => { + return
      {children}
    ; }); -class SortableComponent extends Component { +class App extends Component { state = { items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', 'Item 6'], }; - onSortEnd = ({oldIndex, newIndex}) => { - const {items} = this.state; - this.setState({ + onSortEnd = ({oldIndex, newIndex}) => { + this.setState(({items}) => ({ items: arrayMove(items, oldIndex, newIndex), - }); + })); }; + render() { const {items} = this.state; - return ; + return ( + + {items.map((value, index) => ( + + ))} + + ); } } -render(, document.getElementById('root')); +render(, document.getElementById('root')); diff --git a/examples/infinite-list.js b/examples/infinite-list.js deleted file mode 100644 index 7edc9e91b..000000000 --- a/examples/infinite-list.js +++ /dev/null @@ -1,49 +0,0 @@ -import React, {Component} from 'react'; -import {render} from 'react-dom'; -import {SortableContainer, SortableElement, arrayMove} from 'react-sortable-hoc'; -import Infinite from 'react-infinite'; - -const SortableItem = SortableElement(({height, value}) => { - return ( -
  • - {value} -
  • - ); -}); - -const SortableList = SortableContainer(({items}) => { - return ( - height)}> - {items.map(({value, height}, index) => ( - - ))} - - ); -}); - -class SortableComponent extends Component { - state = { - items: [ - {value: 'Item 1', height: 89}, - {value: 'Item 2', height: 59}, - {value: 'Item 3', height: 130}, - {value: 'Item 4', height: 59}, - {value: 'Item 5', height: 200}, - {value: 'Item 6', height: 150}, - ], - }; - onSortEnd = ({oldIndex, newIndex}) => { - const {items} = this.state; - - this.setState({ - items: arrayMove(items, oldIndex, newIndex), - }); - }; - render() { - const {items} = this.state; - - return ; - } -} - -render(, document.getElementById('root')); diff --git a/examples/react-infinite.js b/examples/react-infinite.js new file mode 100644 index 000000000..ae3fa865f --- /dev/null +++ b/examples/react-infinite.js @@ -0,0 +1,54 @@ +import React, {Component} from 'react'; +import {render} from 'react-dom'; +import {sortableContainer, sortableElement} from 'react-sortable-hoc'; +import arrayMove from 'array-move'; +import Infinite from 'react-infinite'; + +const SortableItem = sortableElement(({height, value}) => { + return
  • {value}
  • ; +}); + +const SortableInfiniteList = sortableContainer(({items}) => { + return ( + height)} + > + {items.map(({value, height}, index) => ( + + ))} + + ); +}); + +class App extends Component { + state = { + items: [ + {value: 'Item 1', height: 89}, + {value: 'Item 2', height: 59}, + {value: 'Item 3', height: 130}, + {value: 'Item 4', height: 59}, + {value: 'Item 5', height: 200}, + {value: 'Item 6', height: 150}, + ], + }; + + onSortEnd = ({oldIndex, newIndex}) => { + this.setState(({items}) => ({ + items: arrayMove(items, oldIndex, newIndex), + })); + }; + + render() { + const {items} = this.state; + + return ; + } +} + +render(, document.getElementById('root')); diff --git a/examples/virtual-table-columns.js b/examples/react-virtualized-table-columns.js similarity index 56% rename from examples/virtual-table-columns.js rename to examples/react-virtualized-table-columns.js index 8458cdbc4..cb6539dbf 100644 --- a/examples/virtual-table-columns.js +++ b/examples/react-virtualized-table-columns.js @@ -1,27 +1,26 @@ import React, {Component} from 'react'; import {render} from 'react-dom'; import {Table, Column} from 'react-virtualized'; -import {SortableContainer, SortableElement, arrayMove} from 'react-sortable-hoc'; +import {sortableContainer, sortableElement} from 'react-sortable-hoc'; +import arrayMove from 'array-move'; import 'react-virtualized/styles.css'; const ROW_HEIGHT = 30; const HEADER_ROW_HEIGHT = 20; const COL_WIDTH = 100; -const SortableHeader = SortableElement(({children, ...props}) => - React.cloneElement(children, props) +const SortableHeader = sortableElement(({children, ...props}) => + React.cloneElement(children, props), ); -const SortableHeaderRowRenderer = SortableContainer( +const SortableHeaderRowRenderer = sortableContainer( ({className, columns, style}) => (
    {React.Children.map(columns, (column, index) => ( - - {column} - + {column} ))}
    - ) + ), ); class TableWithSortableColumns extends Component { @@ -37,36 +36,44 @@ class TableWithSortableColumns extends Component { {col1: 'row3 col1', col2: 'row3 col2', col3: 'row3 col3'}, ], }; + onSortEnd = ({oldIndex, newIndex}) => { - this.setState(state => ({ - cols: arrayMove(state.cols, oldIndex, newIndex), + this.setState(({cols}) => ({ + cols: arrayMove(cols, oldIndex, newIndex), })); }; + + getRow = ({index}) => { + const {rows} = this.state; + return rows[index]; + }; + + renderHeaderRow = (params) => { + return ( + + ); + }; + render() { const {rows, cols} = this.state; + return ( rows[index]} - headerRowRenderer={params => ( - - )} + rowGetter={this.getRow} + headerRowRenderer={this.renderHeaderRow} > - {cols.map(col => ( - + {cols.map((col) => ( + ))}
    ); diff --git a/examples/react-virtualized.js b/examples/react-virtualized.js new file mode 100644 index 000000000..49071c74d --- /dev/null +++ b/examples/react-virtualized.js @@ -0,0 +1,90 @@ +import React, {Component} from 'react'; +import {render} from 'react-dom'; +import {sortableContainer, sortableElement} from 'react-sortable-hoc'; +import arrayMove from 'array-move'; +import {List} from 'react-virtualized'; + +const SortableItem = sortableElement(({value}) => { + return
  • {value}
  • ; +}); + +class VirtualList extends Component { + renderRow = ({index}) => { + const {items} = this.props; + const {value} = items[index]; + + return ; + }; + + getRowHeight = ({index}) => { + const {items} = this.props; + return items[index].height; + }; + + render() { + const {items, getRef} = this.props; + + return ( + + ); + } +} + +const SortableVirtualList = sortableContainer(VirtualList); + +class App extends Component { + state = { + items: [ + {value: 'Item 1', height: 89}, + {value: 'Item 2', height: 59}, + {value: 'Item 3', height: 130}, + {value: 'Item 4', height: 59}, + {value: 'Item 5', height: 200}, + {value: 'Item 6', height: 150}, + ], + }; + + registerListRef = (listInstance) => { + this.List = listInstance; + }; + + onSortEnd = ({oldIndex, newIndex}) => { + if (oldIndex === newIndex) { + return; + } + + const {items} = this.state; + + this.setState({ + items: arrayMove(items, oldIndex, newIndex), + }); + + // We need to inform React Virtualized that the items have changed heights + // This can either be done by imperatively calling the recomputeRowHeights and + // forceUpdate instance methods on the `List` ref, or by passing an additional prop + // to List that changes whenever the order changes to force it to re-render + this.List.recomputeRowHeights(); + this.List.forceUpdate(); + }; + + render() { + const {items} = this.state; + + return ( + + ); + } +} + +render(, document.getElementById('root')); diff --git a/examples/virtual-list.js b/examples/virtual-list.js deleted file mode 100644 index f705438c3..000000000 --- a/examples/virtual-list.js +++ /dev/null @@ -1,84 +0,0 @@ -import React, {Component} from 'react'; -import {render} from 'react-dom'; -import {SortableContainer, SortableElement, arrayMove} from 'react-sortable-hoc'; -import {List} from 'react-virtualized'; - -const SortableItem = SortableElement(({value}) => { - return ( -
  • - {value} -
  • - ); -}); - -class VirtualList extends Component { - render() { - const {items} = this.props; - - return ( - { - this.List = instance; - }} - rowHeight={({index}) => items[index].height} - rowRenderer={({index}) => { - const {value} = items[index]; - return ; - }} - rowCount={items.length} - width={400} - height={600} - /> - ); - } -} - -/* - * Important note: - * To access the ref of a component that has been wrapped with the SortableContainer HOC, - * you *must* pass in {withRef: true} as the second param. Refs are opt-in. - */ -const SortableList = SortableContainer(VirtualList, {withRef: true}); - -class SortableComponent extends Component { - state = { - items: [ - {value: 'Item 1', height: 89}, - {value: 'Item 2', height: 59}, - {value: 'Item 3', height: 130}, - {value: 'Item 4', height: 59}, - {value: 'Item 5', height: 200}, - {value: 'Item 6', height: 150}, - ], - }; - onSortEnd = ({oldIndex, newIndex}) => { - if (oldIndex !== newIndex) { - const {items} = this.state; - - this.setState({ - items: arrayMove(items, oldIndex, newIndex), - }); - - // We need to inform React Virtualized that the items have changed heights - const instance = this.SortableList.getWrappedInstance(); - - instance.List.recomputeRowHeights(); - instance.forceUpdate(); - } - }; - render() { - const {items} = this.state; - - return ( - { - this.SortableList = instance; - }} - items={items} - onSortEnd={this.onSortEnd} - /> - ); - } -} - -render(, document.getElementById('root')); diff --git a/index.html b/index.html deleted file mode 100644 index af7cf9e14..000000000 --- a/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - React Sortable - - - - -
    - - - diff --git a/index.js b/index.js deleted file mode 100644 index 81492419b..000000000 --- a/index.js +++ /dev/null @@ -1,64 +0,0 @@ -import 'babel-polyfill'; -import React, {Component} from 'react'; -import {render} from 'react-dom'; -import {SortableContainer, SortableElement, arrayMove} from './src/index'; -import range from 'lodash/range'; -import random from 'lodash/random'; - -const SortableItem = SortableElement(({height, value}) => ( -
    - Item {value} -
    -)); - -const SortableList = SortableContainer(({items}) => ( -
    - {items.map(({height, value}, index) => )} -
    -)); - -class Example extends Component { - state = { - items: range(100).map((value) => { - return { - value, - height: random(49, 120) - }; - }) - }; - onSortEnd = ({oldIndex, newIndex}) => { - let {items} = this.state; - - this.setState({ - items: arrayMove(items, oldIndex, newIndex) - }); - }; - render() { - const {items} = this.state; - - return ; - } -} - -render(, - document.getElementById('root') -) diff --git a/package.json b/package.json index 80d3df519..f650a688e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-sortable-hoc", - "version": "0.6.8", + "version": "2.0.0", "description": "Set of higher-order components to turn any list into a sortable, touch-friendly, animated list", "author": { "name": "Clauderic Demers", @@ -8,8 +8,12 @@ }, "user": "clauderic", "homepage": "https://github.com/clauderic/react-sortable-hoc", - "main": "dist/commonjs/index.js", - "jsnext:main": "dist/es6/index.js", + "source": "src/index.js", + "main": "dist/react-sortable-hoc.js", + "umd:main": "dist/react-sortable-hoc.umd.js", + "module": "dist/react-sortable-hoc.esm.js", + "jsnext:main": "dist/react-sortable-hoc.esm.js", + "types": "types/index.d.ts", "license": "MIT", "repository": { "type": "git", @@ -33,87 +37,73 @@ "component" ], "scripts": { - "start": "webpack-dashboard -- node server.js", - "build": "npm run build:commonjs && npm run build:es6 && npm run build:umd", - "build:commonjs": "npm run clean:commonjs && cross-env BABEL_ENV=commonjs babel src --out-dir dist/commonjs --ignore *.example.js,*.test.js", - "build:es6": "npm run clean:es6 && cross-env BABEL_ENV=es6 BABEL_DISABLE_CACHE=1 babel src --out-dir dist/es6 --ignore *.test.js", - "build:umd": "cross-env NODE_ENV=production webpack --config webpack.config.umd.js && cross-env NODE_ENV=production webpack --config webpack.config.umd.js --minify", - "clean": "npm run clean:umd", - "clean:umd": "rimraf dist/umd", - "clean:es6": "rimraf dist/es6", - "clean:commonjs": "rimraf dist/commonjs", - "storybook": "start-storybook -p 9001", - "test": "eslint src/** --ext .js" + "start": "start-storybook -p 9001 -c .storybook", + "build": "rollup -c", + "test": "eslint src/** --ext .js --quiet", + "release": "standard-version --no-verify" + }, + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged" + } }, "dependencies": { - "babel-runtime": "^6.11.6", - "invariant": "^2.2.1", - "lodash": "^4.12.0", + "@babel/runtime": "^7.13.0", + "invariant": "^2.2.4", "prop-types": "^15.5.7" }, "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16.0.0", - "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" + "prop-types": "^15.5.7", + "react": "^16.3.0 || ^17.0.0", + "react-dom": "^16.3.0 || ^17.0.0" }, "devDependencies": { - "@kadira/storybook": "^1.36.0", - "@kadira/storybook-deployer": "^1.0.0", + "@babel/core": "^7.2.2", + "@babel/plugin-proposal-class-properties": "^7.2.3", + "@babel/plugin-transform-runtime": "^7.2.0", + "@babel/preset-env": "^7.2.3", + "@babel/preset-react": "^7.0.0", + "@storybook/addon-options": "^5.1.11", + "@storybook/react": "^5.1.11", + "@storybook/theming": "^5.1.11", + "array-move": "^1.0.0", "autoprefixer": "^6.3.6", - "babel-cli": "^6.9.0", - "babel-core": "^6.3.15", - "babel-eslint": "^7.1.1", - "babel-loader": "^6.2.0", - "babel-plugin-css-modules-transform": "^0.1.0", - "babel-plugin-transform-object-assign": "^6.8.0", - "babel-plugin-transform-runtime": "^6.15.0", - "babel-polyfill": "^6.3.14", - "babel-preset-es2015": "^6.3.13", - "babel-preset-es2015-rollup": "^1.1.1", - "babel-preset-react": "^6.3.13", - "babel-preset-react-hmre": "^1.1.1", - "babel-preset-stage-0": "^6.5.0", - "babel-preset-stage-1": "^6.5.0", - "babel-preset-stage-2": "^6.5.0", - "babel-register": "^6.3.13", + "babel-loader": "^8.0.5", + "babel-plugin-transform-async-to-promises": "^0.8.4", "classnames": "^2.2.5", - "cross-env": "^1.0.7", - "css-loader": "^0.23.1", - "eslint": "^3.16.1", - "eslint-config-react-app": "^0.6.1", - "eslint-plugin-flowtype": "^2.21.0", - "eslint-plugin-import": "^2.0.1", - "eslint-plugin-jsx-a11y": "^4.0.0", - "eslint-plugin-react": "^6.4.1", - "express": "^4.13.3", + "css-loader": "^2.1.0", + "eslint": "^6.2.1", + "eslint-config-prettier": "^6.1.0", + "eslint-plugin-prettier": "^3.1.0", + "eslint-plugin-shopify": "^30.0.1", "extract-text-webpack-plugin": "^1.0.1", "html-webpack-plugin": "^2.16.1", - "isparta-loader": "^2.0.0", - "node-libs-browser": "^0.5.2", - "node-sass": "^3.7.0", - "postcss-loader": "^0.9.1", - "qs": "^6.2.0", - "raw-loader": "^0.5.1", - "react": "^15.4.2", + "husky": "^3.0.4", + "lodash": "^4.12.0", + "node-sass": "^4.11.0", + "postcss": "^7.0.7", + "postcss-loader": "^3.0.0", + "prettier": "^1.18.2", + "pretty-quick": "^1.11.1", + "react": "^16.7.0", "react-addons-pure-render-mixin": "^15.0.2", "react-addons-shallow-compare": "^15.1.0", "react-addons-test-utils": "^15.1.0", - "react-dom": "^15.4.2", - "react-infinite": "^0.9.2", + "react-dom": "^16.7.0", + "react-infinite": "^0.13.0", + "react-inspector": "^3.0.2", "react-tiny-virtual-list": "^2.0.1", "react-virtualized": "^9.2.2", - "redux": "^3.5.2", - "rimraf": "^2.5.2", - "sass-loader": "^3.2.0", - "stack-source-map": "^1.0.4", - "style-loader": "^0.13.1", - "webpack": "^1.9.11", - "webpack-dashboard": "^0.2.1", - "webpack-dev-middleware": "^1.2.0", - "webpack-hot-middleware": "^2.9.1", - "yargs": "^4.7.1" - }, - "xo": { - "esnext": true, - "extends": "xo-react" + "react-window": "^1.6.2", + "rollup": "^1.0.0", + "rollup-plugin-babel": "^4.2.0", + "rollup-plugin-commonjs": "^9.2.0", + "rollup-plugin-filesize": "^6.0.0", + "rollup-plugin-node-resolve": "^4.0.0", + "rollup-plugin-replace": "^2.1.0", + "rollup-plugin-uglify": "^6.0.0", + "sass-loader": "^7.1.0", + "standard-version": "^4.4.0", + "style-loader": "^0.23.1" } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 000000000..e8c8d4273 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,89 @@ +import replace from 'rollup-plugin-replace'; +import resolve from 'rollup-plugin-node-resolve'; +import commonjs from 'rollup-plugin-commonjs'; +import babel from 'rollup-plugin-babel'; +import filesize from 'rollup-plugin-filesize'; +import {uglify} from 'rollup-plugin-uglify'; +import pkg from './package.json'; + +const external = (id) => !id.startsWith('.') && !id.startsWith('/'); + +const babelConfig = ( + {useESModules, targets} = { + useESModules: true, + targets: {browsers: 'last 2 versions'}, + }, +) => ({ + comments: false, + runtimeHelpers: true, + presets: [ + '@babel/preset-react', + [ + '@babel/preset-env', + { + targets, + }, + ], + ], + plugins: [ + '@babel/plugin-proposal-class-properties', + ['@babel/transform-runtime', {useESModules, regenerator: false}], + ['babel-plugin-transform-async-to-promises', {inlineHelpers: true}], + ], + exclude: 'node_modules/**', +}); + +const umdConfig = ({minify} = {}) => ({ + input: pkg.source, + external: ['react', 'react-dom', 'prop-types'], + output: { + name: 'SortableHOC', + file: minify ? pkg["umd:main"].replace('.js', '.min.js') : pkg["umd:main"], + format: 'umd', + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'prop-types': 'PropTypes', + }, + }, + plugins: [ + resolve(), + babel( + babelConfig({ + targets: {browsers: ['last 2 versions', 'safari >= 7']}, + }), + ), + replace({ + 'process.env.NODE_ENV': JSON.stringify( + minify ? 'production' : 'development', + ), + }), + commonjs(), + minify ? uglify() : { }, + filesize(), + ], +}); + +const rollupConfig = [ + // Browser-friendly UMD builds + umdConfig(), + umdConfig({minify: true}), + + // CommonJS + { + input: pkg.source, + external, + output: [{file: pkg.main, format: 'cjs'}], + plugins: [resolve(), babel(babelConfig({useESModules: false})), filesize()], + }, + + // ES module + { + input: pkg.source, + external, + output: [{file: pkg.module, format: 'esm'}], + plugins: [resolve(), babel(babelConfig()), filesize()], + }, +]; + +export default rollupConfig; diff --git a/server.js b/server.js deleted file mode 100644 index 3caca2fb9..000000000 --- a/server.js +++ /dev/null @@ -1,23 +0,0 @@ -var webpack = require('webpack') -var webpackDevMiddleware = require('webpack-dev-middleware') -var webpackHotMiddleware = require('webpack-hot-middleware') -var config = require('./webpack.config.dev') - -var app = new (require('express'))() -var port = 3001 - -var compiler = webpack(config) -app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) -app.use(webpackHotMiddleware(compiler)) - -app.get("/", function(req, res) { - res.sendFile(__dirname + '/index.html') -}) - -app.listen(port, function(error) { - if (error) { - console.error(error) - } else { - console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) - } -}) diff --git a/src/.stories/Storybook.scss b/src/.stories/Storybook.scss index 84d8eb9ed..9c62f41bd 100644 --- a/src/.stories/Storybook.scss +++ b/src/.stories/Storybook.scss @@ -1,183 +1,249 @@ @import url(https://fonts.googleapis.com/css?family=Montserrat:400); +$focusedOutlineColor: #4c9ffe; + .root { - display: flex; - height: 100%; - box-sizing: border-box; - flex-direction: column; - justify-content: center; - align-items: center; + display: flex; + height: 100%; + box-sizing: border-box; + flex-direction: column; + justify-content: center; + align-items: center; } // Base styles .list { - width: 400px; - height: 600px; - overflow: auto; - -webkit-overflow-scrolling: touch; - border: 1px solid #999; + width: 400px; + height: 600px; + overflow: auto; + -webkit-overflow-scrolling: touch; + border: 1px solid #999; } .item { - position: relative; - border-bottom: 1px solid #999; + position: relative; + border-bottom: 1px solid #999; + + cursor: grab; + touch-action: manipulation; + + &.sorting { + pointer-events: none; + } +} + +.containsDragHandle { + cursor: default; } // Stylized .stylizedList { - position: relative; - z-index: 0; - background-color: #F3F3F3; - border: 1px solid #EFEFEF; - border-radius: 3px; - outline: none; + position: relative; + z-index: 0; + background-color: #f3f3f3; + border: 1px solid #efefef; + border-radius: 3px; + outline: none; } + .stylizedItem { - display: flex; - align-items: center; - width: 100%; - padding: 0 20px; - background-color: #FFF; - border-bottom: 1px solid #EFEFEF; - box-sizing: border-box; - user-select: none; + display: flex; + align-items: center; + width: 100%; + padding: 0 20px; + background-color: #fff; + border-bottom: 1px solid #efefef; + box-sizing: border-box; + user-select: none; + outline: none; + + color: #333; + font-weight: 400; + + &:focus:not(.containsDragHandle) { + text-indent: -2px; + border: 2px solid $focusedOutlineColor; + } +} - color: #333; - font-weight: 400; +.disabled { + cursor: not-allowed; + opacity: 0.5; } // Drag handle +.handleWrapper { + width: 18px; + height: 18px; + outline: none; +} + .handle { - display: block; - width: 18px; - height: 18px; - background-image: url('data:image/svg+xml;charset=utf-8,'); - background-size: contain; - background-repeat: no-repeat; - opacity: 0.25; - margin-right: 20px; - cursor: row-resize; + display: block; + width: 18px; + height: 18px; + margin-right: 20px; + overflow: hidden; + + > svg { + opacity: 0.3; + } + + cursor: grab; } // Horizontal list .horizontalList { - display: flex; - width: 600px; - height: 300px; - white-space: nowrap; + display: flex; + width: 600px; + height: 300px; + white-space: nowrap; } .horizontalItem { - display: flex; - flex-shrink: 0; - align-items: center; - justify-content: center; - width: 200px; - border-right: 1px solid #EFEFEF; - border-bottom: 0; + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 200px; + border-right: 1px solid #efefef; + border-bottom: 0; } // Grid .grid { - display: block; - width: 130 * 4px; - height: 350px; - white-space: nowrap; - border: 0; - background-color: transparent; + display: grid; + height: 130 * 3px + 20px; + grid-gap: 10px; + grid-template-columns: auto auto auto auto; + width: auto; + white-space: nowrap; + border: 0; + background-color: transparent; } .gridItem { - float: left; - width: 130px; - padding: 8px; - background: transparent; - border: 0; + width: 130px; + height: 130px; + padding: 0; + border: none; + background-color: transparent; - .wrapper { - display: flex; - align-items: center; - justify-content: center; + .wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: #fff; + transform: scale(1); + font-size: 28px; + + span { + display: none; + } + } +} - width: 100%; - height: 100%; - background: #FFF; - border: 1px solid #EFEFEF; +.gridVariableSized { + .gridItem { + &[data-index='0'] { + width: auto !important; + height: auto !important; + grid-column-end: span 2; + grid-row-end: span 2; + } - font-size: 28px; + &.sorting { + .wrapper { + transition: transform 150ms ease-in-out; + } + } + } +} - span { - display: none; - } +.gridItemVariableSized { + &[data-index='0'] { + .wrapper { + font-size: 56px; } + } } // Nested .category { - height: auto; + height: auto; - .categoryHeader { - display: flex; - flex-flow: row nowrap; - align-items: center; - padding: 10px 14px; - background: #F9F9F9; - border-bottom: 1px solid #EFEFEF; - } + .categoryHeader { + display: flex; + flex-flow: row nowrap; + align-items: center; + padding: 10px 14px; + background: #f9f9f9; + border-bottom: 1px solid #efefef; + user-select: none; + } - .categoryList { - height: auto; - } + .categoryList { + height: auto; + } } // Divider .divider { - padding: 10px 20px; - background: #F9F9F9; - border-bottom: 1px solid #EFEFEF; - text-transform: uppercase; - font-size: 14px; - color: #333; + padding: 10px 20px; + background: #f9f9f9; + border-bottom: 1px solid #efefef; + text-transform: uppercase; + font-size: 14px; + color: #333; } - // Helper styles .helper { - box-shadow: 0 5px 5px -5px rgba(0,0,0,0.2), 0 -5px 5px -5px rgba(0,0,0,0.2); + box-shadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2), + 0 -5px 5px -5px rgba(0, 0, 0, 0.2); + + cursor: grabbing; } + .stylizedHelper { - box-shadow: 0 5px 5px -5px rgba(0,0,0,0.2), 0 -5px 5px -5px rgba(0,0,0,0.2); - background-color: rgba(255,255,255,0.8); - cursor: row-resize; + &:not(.gridItem), + &.gridItem .wrapper { + border: 1px solid #efefef; + box-shadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2); + background-color: rgba(255, 255, 255, 0.9); + border-radius: 3px; &.horizontalItem { - cursor: col-resize; + cursor: col-resize; } - &.gridItem { - background-color: transparent; - white-space: nowrap; - box-shadow: none; - - .wrapper { - background-color: rgba(255,255,255,0.8); - box-shadow: 0 0 7px rgba(0,0,0,0.15) - } + + &:focus { + box-shadow: 0 0px 5px 1px $focusedOutlineColor; } + } + + &.gridItem .wrapper { + transition: transform 150ms ease-in-out; + } } .shrinkedHelper { - height: 20px !important; + height: 20px !important; } :global { - body { - font-family: 'Montserrat', 'Helvetica Neue', 'Helvetica', arial, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: rgba(#F4F5F9, 0.7); - } - html, body, #root { - height: 100%; - margin: 0; - } + body { + font-family: 'Montserrat', 'Helvetica Neue', 'Helvetica', arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: rgba(#f4f5f9, 0.7); + } + html, + body, + #root { + height: 100%; + margin: 0; + } } diff --git a/src/.stories/grouping-items/Item/Item.js b/src/.stories/grouping-items/Item/Item.js new file mode 100644 index 000000000..a163bd696 --- /dev/null +++ b/src/.stories/grouping-items/Item/Item.js @@ -0,0 +1,46 @@ +import React from 'react'; +import classNames from 'classnames'; +import {sortableElement} from '../../../../src'; + +import styles from './Item.scss'; + +const ENTER_KEY = 13; + +function Item(props) { + const { + dragging, + sorting, + onClick, + selected, + selectedItemsCount, + value, + } = props; + const shouldRenderItemCountBadge = dragging && selectedItemsCount > 1; + + return ( +
    onClick(value)} + onKeyPress={(event) => { + if (event.which === ENTER_KEY) { + onClick(value); + } + }} + tabIndex={0} + > + Item {value} + {shouldRenderItemCountBadge ? : null} +
    + ); +} + +function Badge(props) { + return
    {props.count}
    ; +} + +export default sortableElement(Item); diff --git a/src/.stories/grouping-items/Item/Item.scss b/src/.stories/grouping-items/Item/Item.scss new file mode 100644 index 000000000..f745a1d07 --- /dev/null +++ b/src/.stories/grouping-items/Item/Item.scss @@ -0,0 +1,86 @@ +$color: #333; +$white: #fff; +$backgroundColor: $white; + +$boxShadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2); + +$fontWeight-regular: 400; +$fontWeight-bold: 600; + +$borderRadius: 3px; +$borderWidth: 1px; +$borderColor: #efefef; + +$selectedColor: $white; +$selectedBackgroundColor: rgba(216, 232, 251, 0.9); +$selectedBorderColor: #bbcee8; + +$badgeColor: $white; +$badgeBackgroundColor: #f75959; +$badgeBorderColor: #da4553; + +$focusedOutlineColor: #4c9ffe; + +.Item { + display: flex; + align-items: center; + width: 100%; + height: 59px; + padding: 0 20px; + background-color: $backgroundColor; + border-bottom: $borderWidth solid #efefef; + box-sizing: border-box; + user-select: none; + outline: none; + + color: $color; + font-weight: $fontWeight-regular; + + cursor: grab; + + &:last-child { + border-bottom: none; + } + + &.selected { + background: $selectedBackgroundColor; + border-bottom-color: $selectedBorderColor; + + &:focus { + border-bottom-color: $focusedOutlineColor; + } + } + + &.sorting { + pointer-events: none; + } + + &.dragging { + border-radius: $borderRadius; + border: $borderWidth solid #efefef; + box-shadow: $boxShadow; + + &:focus { + box-shadow: 0 0px 5px 1px $focusedOutlineColor; + } + } + + &:focus { + text-indent: -2px; + border: 2px solid $focusedOutlineColor; + } +} + +.Badge { + position: absolute; + top: -8px; + right: -8px; + padding: 0.35em 0.5em; + border-radius: 0.3rem; + + color: $badgeColor; + font-size: 0.8em; + font-weight: $fontWeight-bold; + background-color: $badgeBackgroundColor; + border: $borderWidth solid $badgeBorderColor; +} diff --git a/src/.stories/grouping-items/Item/index.js b/src/.stories/grouping-items/Item/index.js new file mode 100644 index 000000000..01ed425e2 --- /dev/null +++ b/src/.stories/grouping-items/Item/index.js @@ -0,0 +1,3 @@ +import Item from './Item'; + +export default Item; diff --git a/src/.stories/grouping-items/List/List.js b/src/.stories/grouping-items/List/List.js new file mode 100644 index 000000000..240bb1ad3 --- /dev/null +++ b/src/.stories/grouping-items/List/List.js @@ -0,0 +1,32 @@ +import React from 'react'; +import {sortableContainer} from '../../../../src'; + +import Item from '../Item'; + +import styles from './List.scss'; + +function List({items, isSorting, selectedItems, sortingItemKey, onItemSelect}) { + return ( +
    + {items.map((value, index) => { + const isSelected = selectedItems.includes(value); + const itemIsBeingDragged = sortingItemKey === value; + + return ( + + ); + })} +
    + ); +} + +export default sortableContainer(List); diff --git a/src/.stories/grouping-items/List/List.scss b/src/.stories/grouping-items/List/List.scss new file mode 100644 index 000000000..f26d003d3 --- /dev/null +++ b/src/.stories/grouping-items/List/List.scss @@ -0,0 +1,16 @@ +$backgroundColor: #f3f3f3; +$borderColor: #efefef; +$borderWidth: 1px; + +.List { + position: relative; + width: 400px; + height: 600px; + overflow: auto; + -webkit-overflow-scrolling: touch; + z-index: 0; + background-color: $backgroundColor; + border: $borderWidth solid $borderColor; + border-radius: 3px; + outline: none; +} diff --git a/src/.stories/grouping-items/List/index.js b/src/.stories/grouping-items/List/index.js new file mode 100644 index 000000000..1e2ddb439 --- /dev/null +++ b/src/.stories/grouping-items/List/index.js @@ -0,0 +1,3 @@ +import List from './List'; + +export default List; diff --git a/src/.stories/grouping-items/index.js b/src/.stories/grouping-items/index.js new file mode 100644 index 000000000..70102ae97 --- /dev/null +++ b/src/.stories/grouping-items/index.js @@ -0,0 +1,122 @@ +import React from 'react'; +import arrayMove from 'array-move'; +import {generateItems} from './utils'; + +import SortableList from './List'; + +class GroupedItems extends React.Component { + state = { + selectedItems: [], + items: generateItems(50), + }; + + render() { + const {items, isSorting, selectedItems, sortingItemKey} = this.state; + + return ( + + ); + } + + filterItems = (value) => { + const {selectedItems, sortingItemKey, isSorting} = this.state; + + // Do not hide the ghost of the element currently being sorted + if (sortingItemKey === value) { + return true; + } + + // Hide the other items that are selected + if (isSorting && selectedItems.includes(value)) { + return false; + } + + // Do not hide any other items + return true; + }; + + handleUpdateBeforeSortStart = ({index}) => { + return new Promise((resolve) => + this.setState( + ({items}) => ({ + sortingItemKey: items[index], + isSorting: true, + }), + resolve, + ), + ); + }; + + handleSortStart() { + document.body.style.cursor = 'grabbing'; + } + + handleSortEnd = ({oldIndex, newIndex}) => { + const {selectedItems} = this.state; + let newItems; + + if (selectedItems.length) { + const items = this.state.items.filter( + (value) => !selectedItems.includes(value), + ); + + newItems = [ + ...items.slice(0, newIndex), + ...selectedItems, + ...items.slice(newIndex, items.length), + ]; + } else { + newItems = arrayMove(this.state.items, oldIndex, newIndex); + } + + this.setState({ + items: newItems, + isSorting: false, + sortingItemKey: null, + selectedItems: [], + }); + + document.body.style.cursor = ''; + }; + + handleItemSelect = (item) => { + this.setState(({selectedItems}) => { + if (selectedItems.includes(item)) { + return { + selectedItems: selectedItems.filter((value) => value !== item), + }; + } + + return { + selectedItems: [...selectedItems, item], + }; + }); + }; + + handleShouldCancelStart = (event) => { + const {items, selectedItems} = this.state; + const item = items[event.target.sortableInfo.index]; + + // Never cancel start if there are no selected items + if (!selectedItems.length) { + return false; + } + + // If there are selected items, we want to cancel sorting + // from starting when dragging elements that are not selected + return !selectedItems.includes(item); + }; +} + +export default GroupedItems; diff --git a/src/.stories/grouping-items/utils.js b/src/.stories/grouping-items/utils.js new file mode 100644 index 000000000..828fc4419 --- /dev/null +++ b/src/.stories/grouping-items/utils.js @@ -0,0 +1,3 @@ +export function generateItems(length) { + return Array.from(Array(length), (_, index) => index.toString()); +} diff --git a/src/.stories/index.js b/src/.stories/index.js index 11a2f0783..5981f3cb9 100644 --- a/src/.stories/index.js +++ b/src/.stories/index.js @@ -1,78 +1,146 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; -import {storiesOf} from '@kadira/storybook'; +import {storiesOf} from '@storybook/react'; import style from './Storybook.scss'; -import {SortableContainer, SortableElement, SortableHandle, arrayMove} from '../index'; +import {SortableContainer, SortableElement, SortableHandle} from '../index'; +import arrayMove from 'array-move'; import VirtualList from 'react-tiny-virtual-list'; -import { - defaultTableRowRenderer, - Column, - Table, - List, -} from 'react-virtualized'; -import 'react-virtualized/styles.css'; +import {FixedSizeList, VariableSizeList} from 'react-window'; +import {defaultTableRowRenderer, Column, Table, List} from 'react-virtualized'; +import '!style-loader!css-loader!react-virtualized/styles.css'; import Infinite from 'react-infinite'; import range from 'lodash/range'; import random from 'lodash/random'; import classNames from 'classnames'; +import GroupedItems from './grouping-items'; +import InteractiveElements from './interactive-elements-stress-test'; + function getItems(count, height) { var heights = [65, 110, 140, 65, 90, 65]; - return range(count).map(value => { + return range(count).map((value) => { return { value, - height: height || heights[random(0, heights.length - 1)], + height: height == null ? heights[random(0, heights.length - 1)] : height, }; }); } -const Handle = SortableHandle(() =>
    ); +const Handle = SortableHandle(({tabIndex}) => ( +
    + + + +
    +)); + +const Item = SortableElement( + ({ + tabbable, + className, + isDisabled, + height, + style: propStyle, + shouldUseDragHandle, + value, + itemIndex, + isSorting, + }) => { + const bodyTabIndex = tabbable && !shouldUseDragHandle ? 0 : -1; + const handleTabIndex = tabbable && shouldUseDragHandle ? 0 : -1; -const Item = SortableElement(props => { - return ( -
    - {props.shouldUseDragHandle && } -
    - Item {props.value} + return ( +
    + {shouldUseDragHandle && } +
    + Item {value} +
    -
    - ); -}); + ); + }, +); -const SortableList = SortableContainer(({ - className, - items, - itemClass, - shouldUseDragHandle, -}) => { - return ( -
    - {items.map(({value, height}, index) => ( - - ))} -
    - ); -}); +const SortableList = SortableContainer( + ({ + className, + items, + disabledItems = [], + itemClass, + isSorting, + shouldUseDragHandle, + type, + }) => { + return ( +
    + {items.map(({value, height}, index) => { + const disabled = disabledItems.includes(value); + + return ( + + ); + })} +
    + ); + }, +); + +class SortableListWithCustomContainer extends React.Component { + state = { + container: null, + }; + + render() { + const {container} = this.state; + + return ( +
    + +
    + ); + } + + setContainerNode = (node) => { + this.setState({container: node}); + }; +} + +const Category = SortableElement((props) => { + const tabIndex = props.tabbable ? 0 : -1; -const Category = SortableElement(props => { return (
    - + Category {props.value}
    { }); class ListWrapper extends Component { - constructor({items}) { - super(); - this.state = { - items, - isSorting: false, - }; - } + state = { + items: this.props.items, + isSorting: false, + }; + static propTypes = { items: PropTypes.array, className: PropTypes.string, @@ -104,31 +170,44 @@ class ListWrapper extends Component { onSortEnd: PropTypes.func, component: PropTypes.func, shouldUseDragHandle: PropTypes.bool, + disabledItems: PropTypes.arrayOf(PropTypes.string), }; + static defaultProps = { className: classNames(style.list, style.stylizedList), itemClass: classNames(style.item, style.stylizedItem), width: 400, height: 600, }; - onSortStart = () => { + + onSortStart = (sortEvent, nativeEvent) => { const {onSortStart} = this.props; this.setState({isSorting: true}); + document.body.style.cursor = 'grabbing'; + if (onSortStart) { - onSortStart(this.refs.component); + onSortStart(sortEvent, nativeEvent, this.refs.component); } }; - onSortEnd = ({oldIndex, newIndex}) => { + + onSortEnd = (sortEvent, nativeEvent) => { const {onSortEnd} = this.props; + const {oldIndex, newIndex} = sortEvent; const {items} = this.state; - this.setState({items: arrayMove(items, oldIndex, newIndex), isSorting: false}); + this.setState({ + items: arrayMove(items, oldIndex, newIndex), + isSorting: false, + }); + + document.body.style.cursor = ''; if (onSortEnd) { - onSortEnd(this.refs.component); + onSortEnd(sortEvent, nativeEvent, this.refs.component); } }; + render() { const Component = this.props.component; const {items, isSorting} = this.state; @@ -145,40 +224,77 @@ class ListWrapper extends Component { } } +const SortableReactWindow = (Component) => + SortableContainer( + class ReactWindowList extends React.Component { + render() { + const {className, items, itemHeight, height, width} = this.props; -const SortableVirtualList = SortableContainer(({ - className, - items, - height, - width, - itemHeight, - itemClass, - sortingIndex, -}) => { - return ( - items[index].height} - estimatedItemSize={itemHeight} - renderItem={({index, style}) => { + return ( + items[index].height : itemHeight + } + itemCount={items.length} + width={width} + height={height} + children={this.renderRow} + /> + ); + } + + renderRow = ({index, style}) => { + const {items, itemClass, isSorting} = this.props; const {value, height} = items[index]; + return ( ); - }} - itemCount={items.length} - width={width} - height={height} - /> + }; + }, + {withRef: true}, ); -}); + +const SortableVirtualList = SortableContainer( + ({className, items, height, width, itemHeight, itemClass, isSorting}) => { + return ( + items[index].height} + estimatedItemSize={itemHeight} + renderItem={({index, style}) => { + const {value, height} = items[index]; + return ( + + ); + }} + itemCount={items.length} + width={width} + height={height} + /> + ); + }, +); // Function components cannot have refs, so we'll be using a class for React Virtualized class VirtualizedListWrapper extends Component { @@ -190,10 +306,11 @@ class VirtualizedListWrapper extends Component { width, itemHeight, itemClass, + isSorting, } = this.props; return ( items[index].height} estimatedRowSize={itemHeight} @@ -201,12 +318,14 @@ class VirtualizedListWrapper extends Component { const {value, height} = items[index]; return ( ); }} @@ -218,7 +337,9 @@ class VirtualizedListWrapper extends Component { } } -const SortableVirtualizedList = SortableContainer(VirtualizedListWrapper, {withRef: true}); +const SortableVirtualizedList = SortableContainer(VirtualizedListWrapper, { + withRef: true, +}); const SortableTable = SortableContainer(Table, {withRef: true}); const SortableRowRenderer = SortableElement(defaultTableRowRenderer); @@ -247,7 +368,9 @@ class TableWrapper extends Component { return ( ReactDOM.findDOMNode(wrappedInstance.Grid)} + getContainer={(wrappedInstance) => + ReactDOM.findDOMNode(wrappedInstance.Grid) + } gridClassName={className} headerHeight={itemHeight} height={height} @@ -257,7 +380,7 @@ class TableWrapper extends Component { rowCount={items.length} rowGetter={({index}) => items[index]} rowHeight={itemHeight} - rowRenderer={props => } + rowRenderer={(props) => } width={width} > @@ -267,92 +390,84 @@ class TableWrapper extends Component { } } -const SortableInfiniteList = SortableContainer(({ - className, - items, - itemClass, -}) => { - return ( - height)} - > - {items.map(({value, height}, index) => ( - - ))} - - ); -}); - -const ShrinkingSortableList = SortableContainer(({ - className, - isSorting, - items, - itemClass, - shouldUseDragHandle, -}) => { - return ( -
    - {items.map(({value, height}, index) => ( - - ))} -
    - ); -}); +const SortableInfiniteList = SortableContainer( + ({className, items, itemClass, isSorting}) => { + return ( + height)} + // for react-infinite, a larger preload is better for keyboard sorting + preloadBatchSize={Infinite.containerHeightScaleFactor(2)} + preloadAdditionalHeight={Infinite.containerHeightScaleFactor(2)} + > + {items.map(({value, height}, index) => ( + + ))} + + ); + }, +); -const NestedSortableList = SortableContainer(({ - className, - items, - isSorting, -}) => { - return ( -
    - {items.map((value, index) => ( - - ))} -
    - ); -}); +const ShrinkingSortableList = SortableContainer( + ({className, isSorting, items, itemClass, shouldUseDragHandle}) => { + return ( +
    + {items.map(({value, height}, index) => ( + + ))} +
    + ); + }, +); -storiesOf('Basic Configuration', module) - .add('Basic usage', () => { +const NestedSortableList = SortableContainer( + ({className, items, isSorting}) => { return ( -
    - +
    + {items.map((value, index) => ( + + ))}
    ); - }) - .add('Drag handle', () => { + }, +); + +storiesOf('General | Layout / Vertical list', module) + .add('Basic setup', () => { return (
    ); }) - .add('Elements of varying heights', () => { + .add('Variable heights', () => { return (
    ); }) - .add('Elements that shrink', () => { - const getHelperDimensions = ({node}) => ({height: 20, width: node.offsetWidth}); + .add('Nested Lists', () => { return (
    ); - }) - .add('Horizontal', () => { + }); + +storiesOf('General | Layout / Horizontal list', module).add( + 'Basic setup', + () => { return (
    ); - }) - .add('Grid', () => { + }, +); + +storiesOf('General | Layout / Grid', module) + .add('Basic setup', () => { + const transformOrigin = { + x: 0, + y: 0, + }; + return (
    ); }) - .add('Nested Lists', () => { + .add('Large first item', () => { return (
    { + const nodeBoundingClientRect = node.getBoundingClientRect(); + const helperWrapperNode = helper.childNodes[0]; + const transformOrigin = { + x: + ((event.clientX - nodeBoundingClientRect.left) / + nodeBoundingClientRect.width) * + 100, + y: + ((event.clientY - nodeBoundingClientRect.top) / + nodeBoundingClientRect.height) * + 100, + }; + + helperWrapperNode.style.transformOrigin = `${transformOrigin.x}% ${transformOrigin.y}%`; + }} + onSortOver={({nodes, newIndex, index, helper}) => { + const finalNodes = arrayMove(nodes, index, newIndex); + const oldNode = nodes[index].node; + const newNode = nodes[newIndex].node; + const helperScale = newNode.offsetWidth / oldNode.offsetWidth; + const helperWrapperNode = helper.childNodes[0]; + + helperWrapperNode.style.transform = `scale(${helperScale})`; + + finalNodes.forEach(({node}, i) => { + const oldNode = nodes[i].node; + const scale = oldNode.offsetWidth / node.offsetWidth; + const wrapperNode = node.querySelector(`.${style.wrapper}`); + + wrapperNode.style.transform = `scale(${scale})`; + wrapperNode.style.transformOrigin = + newIndex > i ? '0 0' : '100% 0'; + }); + }} + onSortEnd={({nodes}) => { + nodes.forEach(({node}) => { + const wrapperNode = node.querySelector(`.${style.wrapper}`); + + wrapperNode.style.transform = ''; + }); + }} />
    ); }); -storiesOf('Advanced', module) +storiesOf('General | Configuration / Options', module) + .add('Drag handle', () => { + return ( +
    + +
    + ); + }) + .add('Disabled items', () => { + return ( +
    + +
    + ); + }) .add('Press delay (200ms)', () => { return (
    @@ -450,7 +656,6 @@ storiesOf('Advanced', module) items={getItems(50)} helperClass={style.stylizedHelper} lockAxis={'y'} - lockToContainerEdges={true} lockOffset={['0%', '100%']} />
    @@ -466,9 +671,20 @@ storiesOf('Advanced', module) helperClass={style.stylizedHelper} /> ); + }) + .add('Custom sortable helper container', () => { + return ( +
    + +
    + ); }); -storiesOf('Customization', module) +storiesOf('General | Configuration / Customization', module) .add('Minimal styling', () => { return (
    @@ -507,8 +723,11 @@ storiesOf('Customization', module) ); }); -storiesOf('react-tiny-virtual-list', module) - .add('Basic usage', () => { +storiesOf( + 'Advanced examples | Virtualization libraries / react-tiny-virtual-list', + module, +) + .add('Basic setup', () => { return (
    ); }) - .add('Elements of varying heights', () => { + .add('Variable heights', () => { return (
    { +storiesOf('Advanced examples | Virtualization libraries / react-window', module) + .add('Basic setup', () => { + return ( +
    + { + // We need to inform React Window that the order of the items has changed + const instance = ref.getWrappedInstance(); + const list = instance.refs.VirtualList; + + list.forceUpdate(); + }} + /> +
    + ); + }) + .add('Variable heights', () => { + return ( +
    + { + // We need to inform React Window that the item heights have changed + const instance = ref.getWrappedInstance(); + const list = instance.refs.VirtualList; + + list.resetAfterIndex(0); + }} + /> +
    + ); + }); + +storiesOf( + 'Advanced examples | Virtualization libraries / react-virtualized', + module, +) + .add('Basic setup', () => { return (
    ); }) - .add('Elements of varying heights', () => { + .add('Variable heights', () => { return (
    { + onSortEnd={(_sortEvent, _nativeEvent, ref) => { // We need to inform React Virtualized that the item heights have changed const instance = ref.getWrappedInstance(); - const vs = instance.refs.vs; + const list = instance.refs.VirtualList; - vs.recomputeRowHeights(); + list.recomputeRowHeights(); instance.forceUpdate(); }} />
    ); }) - .add('Table usage', () => { + .add('Table', () => { return (
    { +storiesOf( + 'Advanced examples | Virtualization libraries / react-infinite', + module, +) + .add('Basic setup', () => { return (
    ); }) - .add('Elements of varying heights', () => { + .add('Variable heights', () => { return (
    ); }); + +storiesOf('Advanced examples | Re-rendering before sorting', module) + .add('Grouping items', () => ( +
    + +
    + )) + .add('Elements that shrink', () => { + const getHelperDimensions = ({node}) => ({ + height: 20, + width: node.offsetWidth, + }); + return ( +
    + +
    + ); + }); + +storiesOf('Stress Testing | Nested elements', module).add( + 'Interactive elements', + () => ( +
    + +
    + ), +); diff --git a/src/.stories/interactive-elements-stress-test/Item/Item.js b/src/.stories/interactive-elements-stress-test/Item/Item.js new file mode 100644 index 000000000..613cb4a32 --- /dev/null +++ b/src/.stories/interactive-elements-stress-test/Item/Item.js @@ -0,0 +1,16 @@ +import React from 'react'; +import {sortableElement} from '../../../../src'; + +import styles from './Item.scss'; + +function Item(props) { + const {children} = props; + + return ( +
    + {children} +
    + ); +} + +export default sortableElement(Item); diff --git a/src/.stories/interactive-elements-stress-test/Item/Item.scss b/src/.stories/interactive-elements-stress-test/Item/Item.scss new file mode 100644 index 000000000..6725b4893 --- /dev/null +++ b/src/.stories/interactive-elements-stress-test/Item/Item.scss @@ -0,0 +1,62 @@ +$color: #333; +$white: #fff; +$backgroundColor: $white; + +$padding: 20px; + +$boxShadow: 0 5px 5px -5px rgba(0, 0, 0, 0.2); + +$fontWeight-regular: 400; +$fontWeight-bold: 600; + +$borderRadius: 3px; +$borderWidth: 1px; +$borderColor: #efefef; + +$focusedOutlineColor: #4c9ffe; + +.root { + display: block; + width: 250px; + padding: $padding; + background-color: $backgroundColor; + border-bottom: $borderWidth solid #efefef; + box-sizing: border-box; + user-select: none; + + color: $color; + font-family: sans-serif; + font-weight: $fontWeight-regular; + + > * { + display: block; + width: 100%; + font-size: 14px; + } + + > input, + > textarea { + padding: 5px; + border: 1px solid #e0e0e0; + box-sizing: border-box; + } + + label { + input { + margin-right: 0.5em; + } + } + + &:focus { + outline: none; + padding: $padding - 2px; + padding-bottom: $padding - 1px; + border: 2px solid $focusedOutlineColor; + } + + &.dragging { + &:focus { + box-shadow: 0 0px 5px 1px $focusedOutlineColor; + } + } +} diff --git a/src/.stories/interactive-elements-stress-test/Item/index.js b/src/.stories/interactive-elements-stress-test/Item/index.js new file mode 100644 index 000000000..01ed425e2 --- /dev/null +++ b/src/.stories/interactive-elements-stress-test/Item/index.js @@ -0,0 +1,3 @@ +import Item from './Item'; + +export default Item; diff --git a/src/.stories/interactive-elements-stress-test/List.js b/src/.stories/interactive-elements-stress-test/List.js new file mode 100644 index 000000000..bce093c73 --- /dev/null +++ b/src/.stories/interactive-elements-stress-test/List.js @@ -0,0 +1,20 @@ +import React from 'react'; +import {sortableContainer} from '../../../src'; + +import Item from './Item'; + +function List({items}) { + return ( +
    + {items.map(([key, children], index) => { + return ( + + {children} + + ); + })} +
    + ); +} + +export default sortableContainer(List); diff --git a/src/.stories/interactive-elements-stress-test/index.js b/src/.stories/interactive-elements-stress-test/index.js new file mode 100644 index 000000000..2acceaf2d --- /dev/null +++ b/src/.stories/interactive-elements-stress-test/index.js @@ -0,0 +1,80 @@ +import React from 'react'; +import arrayMove from 'array-move'; + +import SortableList from './List'; +import ItemStyles from './Item/Item.scss'; + +const items = { + input: , + textarea: