diff --git a/examples/react/modules/routes.js b/examples/react/modules/routes.js index f68a95dcc..2ca07b6f5 100644 --- a/examples/react/modules/routes.js +++ b/examples/react/modules/routes.js @@ -12,6 +12,7 @@ import phases from './phases'; import secret_state from './secret-state'; import random from './random'; import turnorder from './turnorder'; +import ui from './ui'; import threejs from './threejs'; const routes = [ @@ -39,6 +40,10 @@ const routes = [ name: 'Secret State', routes: secret_state.routes, }, + { + name: 'UI', + routes: ui.routes, + }, { name: 'Other Frameworks', routes: threejs.routes, diff --git a/examples/react/modules/ui/components/board.js b/examples/react/modules/ui/components/board.js new file mode 100644 index 000000000..431e4057c --- /dev/null +++ b/examples/react/modules/ui/components/board.js @@ -0,0 +1,79 @@ +/* + * Copyright 2018 The boardgame.io Authors. + * + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import React from 'react'; +import { UI, Card, Deck } from 'boardgame.io/ui'; + +function handler(type, fn) { + return arg => { + console.log(type + ': ' + JSON.stringify(arg)); + if (fn) fn(arg); + }; +} + +class Board extends React.Component { + constructor(props) { + super(props); + + this.state = { + deck1: [1, 2, 3], + deck2: [], + free: [4], + }; + } + + deck1Drop = arg => { + this.setState(s => ({ + free: s.free.filter(t => t != arg), + deck2: s.deck2.filter(t => t != arg), + deck1: [...s.deck1.filter(t => t != arg), arg], + })); + }; + + deck2Drop = arg => { + this.setState(s => ({ + free: s.free.filter(t => t != arg), + deck1: s.deck1.filter(t => t != arg), + deck2: [...s.deck2.filter(t => t != arg), arg], + })); + }; + + render() { + return ( + + + {this.state.deck1.map(c => ( + + ))} + + + + {this.state.deck2.map(c => ( + + ))} + + +
+ {this.state.free.map(c => ( + + ))} +
+
+ ); + } +} + +export default Board; diff --git a/examples/react/modules/ui/components/singleplayer.js b/examples/react/modules/ui/components/singleplayer.js new file mode 100644 index 000000000..427436431 --- /dev/null +++ b/examples/react/modules/ui/components/singleplayer.js @@ -0,0 +1,26 @@ +/* + * Copyright 2017 The boardgame.io Authors. + * + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import React from 'react'; +import { Client } from 'boardgame.io/react'; +import TicTacToe from '../game'; +import Board from './board'; + +const App = Client({ + game: TicTacToe, + board: Board, +}); + +const Singleplayer = () => ( +
+

Cards & Decks

+ +
+); + +export default Singleplayer; diff --git a/examples/react/modules/ui/game.js b/examples/react/modules/ui/game.js new file mode 100644 index 000000000..9b2c157b3 --- /dev/null +++ b/examples/react/modules/ui/game.js @@ -0,0 +1,15 @@ +/* + * Copyright 2017 The boardgame.io Authors + * + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import { Game } from 'boardgame.io/core'; + +const UIDemo = Game({ + name: 'ui-demo', +}); + +export default UIDemo; diff --git a/examples/react/modules/ui/index.js b/examples/react/modules/ui/index.js new file mode 100644 index 000000000..a17874783 --- /dev/null +++ b/examples/react/modules/ui/index.js @@ -0,0 +1,15 @@ +/* + * Copyright 2017 The boardgame.io Authors. + * + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import routes from './routes'; + +// Any other additional setup for this this module + +export default { + routes, +}; diff --git a/examples/react/modules/ui/routes.js b/examples/react/modules/ui/routes.js new file mode 100644 index 000000000..54260e23c --- /dev/null +++ b/examples/react/modules/ui/routes.js @@ -0,0 +1,19 @@ +/* + * Copyright 2017 The boardgame.io Authors. + * + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import Singleplayer from './components/singleplayer'; + +const routes = [ + { + path: '/ui', + text: 'Cards & Decks', + component: Singleplayer, + }, +]; + +export default routes; diff --git a/package-lock.json b/package-lock.json index 289398604..2f725940a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -773,6 +773,20 @@ "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", "dev": true }, + "array.prototype.flat": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz", + "integrity": "sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==", + "dev": true, + "dependencies": { + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "dev": true + } + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -3346,22 +3360,86 @@ "dev": true }, "enzyme": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.0.0.tgz", - "integrity": "sha1-lM42QlTcZUxOYZsl7sxkS/ZIHec=", - "dev": true + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.6.0.tgz", + "integrity": "sha512-onsINzVLGqKIapTVfWkkw6bYvm1o4CyJ9s8POExtQhAkVa4qFDW6DGCQGRy/5bfZYk+gmUbMNyayXiWDzTkHFQ==", + "dev": true, + "dependencies": { + "function.prototype.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz", + "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", + "dev": true + }, + "rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", + "dev": true + } + } }, "enzyme-adapter-react-16": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.0.tgz", - "integrity": "sha1-5+3VU2dDgY3L7zNtQNfaWbOn244=", - "dev": true - }, - "enzyme-adapter-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.0.0.tgz", - "integrity": "sha1-6U7uY9qaeY1JitsRYqIQLtBPxjg=", - "dev": true + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.4.0.tgz", + "integrity": "sha512-sn2zE3g5/LrSNueLFBNOP0ID/YqOJEJb1qyyZ2VdSWbjzbiNDva3IBEntrBP29Wg/EBeSP5yYt5W+nvQ2oeDKg==", + "dev": true, + "dependencies": { + "enzyme-adapter-utils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.7.0.tgz", + "integrity": "sha512-K5FVpGxMlakvvWS6TkwogAzvMRE4pgve6grPzCuraVHRBzgrmeasGDF1CS2rQc7uKo7OF0FQZxaQm8oJAKXFVw==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz", + "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true + }, + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "dev": true + } + } }, "errno": { "version": "0.1.4", @@ -4636,17 +4714,20 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true }, "aproba": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true }, "are-we-there-yet": { @@ -4656,7 +4737,8 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, "brace-expansion": { @@ -4666,27 +4748,32 @@ }, "chownr": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", "dev": true }, "code-point-at": { "version": "1.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, "concat-map": { "version": "0.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, "debug": { @@ -4696,17 +4783,20 @@ }, "deep-extend": { "version": "0.5.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "dev": true }, "delegates": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true }, "fs-minipass": { @@ -4716,7 +4806,8 @@ }, "fs.realpath": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, "gauge": { @@ -4731,12 +4822,14 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true }, "iconv-lite": { "version": "0.4.19", - "bundled": true, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", "dev": true }, "ignore-walk": { @@ -4751,12 +4844,14 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, "ini": { "version": "1.3.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, "is-fullwidth-code-point": { @@ -4766,7 +4861,8 @@ }, "isarray": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, "minimatch": { @@ -4776,7 +4872,8 @@ }, "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, "minipass": { @@ -4791,19 +4888,22 @@ }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "dependencies": { "minimist": { "version": "0.0.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true } } }, "ms": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, "needle": { @@ -4823,7 +4923,8 @@ }, "npm-bundled": { "version": "1.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", "dev": true }, "npm-packlist": { @@ -4838,12 +4939,14 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, "object-assign": { "version": "4.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, "once": { @@ -4853,12 +4956,14 @@ }, "os-homedir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, "osenv": { @@ -4868,12 +4973,14 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true }, "rc": { @@ -4893,27 +5000,32 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true }, "sax": { "version": "1.2.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, "semver": { "version": "5.5.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "dev": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, "string_decoder": { @@ -4933,24 +5045,28 @@ }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, "tar": { "version": "4.4.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.2.tgz", + "integrity": "sha512-BfkE9CciGGgDsATqkikUHrQrraBCO+ke/1f6SFAEMnxyyfN9lxC+nW1NFWMpqH865DhHIy9vQi682gk1X7friw==", "dev": true, "dependencies": { "safe-buffer": { "version": "5.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true } } }, "util-deprecate": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, "wide-align": { @@ -4960,12 +5076,14 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, "yallist": { "version": "3.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", "dev": true } } @@ -5080,6 +5198,12 @@ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "hash-base": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", @@ -5588,6 +5712,12 @@ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dev": true }, + "is-boolean-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", + "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", + "dev": true + }, "is-buffer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", @@ -5707,6 +5837,12 @@ "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", "dev": true }, + "is-number-object": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", + "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", + "dev": true + }, "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", @@ -5824,6 +5960,12 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "is-string": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", + "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", + "dev": true + }, "is-subset": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", @@ -7303,6 +7445,12 @@ "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", "dev": true }, + "lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", + "dev": true + }, "lodash.every": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.every/-/lodash.every-4.6.0.tgz", @@ -8076,6 +8224,12 @@ "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" }, + "object-inspect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", + "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "dev": true + }, "object-is": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", @@ -8088,12 +8242,6 @@ "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", "dev": true }, - "object.assign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.0.4.tgz", - "integrity": "sha1-scnMBE7xuf5jYG/BQau7MuFHMMw=", - "dev": true - }, "object.entries": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", @@ -9139,9 +9287,9 @@ } }, "raf": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.3.2.tgz", - "integrity": "sha1-DBO+C1tJtG921maSSNUnzysC/ic=", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", "dev": true, "dependencies": { "performance-now": { @@ -9231,9 +9379,9 @@ } }, "react": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.0.0.tgz", - "integrity": "sha1-zn348ZQbA28Cssyp29DLHw6FXi0=", + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/react/-/react-16.4.1.tgz", + "integrity": "sha512-3GEs0giKp6E0Oh/Y9ZC60CmYgUPnp7voH9fbjWsvXtYFb4EWtgQub0ADSq0sJR0BbHc4FThLLtzlcFaFXIorwg==", "dev": true, "dependencies": { "fbjs": { @@ -9285,9 +9433,9 @@ } }, "react-dom": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.0.0.tgz", - "integrity": "sha1-nMMHnD3NcNTG4BuEqrKn40wwP1g=", + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.1.tgz", + "integrity": "sha512-1Gin+wghF/7gl4Cqcvr1DxFX2Osz7ugxSwl6gBqCMpdrxHjIFUS7GYxrFftZ9Ln44FHw0JxCFD9YtZsrbR5/4A==", "dev": true, "dependencies": { "fbjs": { @@ -9304,6 +9452,18 @@ } } }, + "react-dragtastic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/react-dragtastic/-/react-dragtastic-2.4.3.tgz", + "integrity": "sha512-AQwmi3Ri9/Fpf8FKabxnsxSh6gRYZHIQqRBubu3NSBvWQ7bOOpqGxlTtxZVkj3QiSg3h2S3xYZoxB27A6dV0xg==", + "dependencies": { + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==" + } + } + }, "react-html-attributes": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/react-html-attributes/-/react-html-attributes-1.4.1.tgz", @@ -9328,6 +9488,12 @@ "integrity": "sha512-aXrxJTfriJtZtv/+A9cFIOpfbt711yCD1Ht3fSn3JyuDaAt1+eLtb01R5d/8gGpyquOVWn/d+eM1VPqFXS7UZA==", "dev": true }, + "react-is": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.4.2.tgz", + "integrity": "sha512-rI3cGFj/obHbBz156PvErrS5xc6f1eWyTwyV4mo0vF2lGgXgS+mm7EKD5buLJq6jNgIagQescGSVG2YzgXt8Yg==", + "dev": true + }, "react-modal": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.1.5.tgz", @@ -9916,12 +10082,6 @@ "integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=", "dev": true }, - "rst-selector-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.2.tgz", - "integrity": "sha512-T5yd2bsA+FVQ5xP8Ga62gXjOnEaMsYhbbslVB+Fe4R9lAZiF7DfTHRyBpV9xEZ772LwstCdDdkHkvkWIr47X8g==", - "dev": true - }, "rsvp": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", @@ -10417,6 +10577,12 @@ "integrity": "sha1-W8+tOfRkm7LQMSkuGbzwtRDUskI=", "dev": true }, + "string.prototype.trim": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", + "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", + "dev": true + }, "stringify-object": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.2.1.tgz", diff --git a/package.json b/package.json index 696321049..c72ba64d5 100644 --- a/package.json +++ b/package.json @@ -77,8 +77,8 @@ "cross-env": "^5.1.1", "css-loader": "^0.28.7", "docsify-cli": "^4.1.12", - "enzyme": "^3.0.0", - "enzyme-adapter-react-16": "^1.0.0", + "enzyme": "^3.6.0", + "enzyme-adapter-react-16": "^1.4.0", "eslint": "^4.7.2", "eslint-plugin-import": "^2.7.0", "eslint-plugin-jest": "^21.1.0", @@ -98,8 +98,9 @@ "mongodb": "^3.0.3", "open-browser-webpack-plugin": "0.0.5", "prettier": "^1.10.2", - "react": "^16.0.0", - "react-dom": "^16.0.0", + "raf": "^3.4.0", + "react": "^16.3.2", + "react-dom": "^16.3.2", "react-router-dom": "^4.2.2", "react-svg-loader": "^2.1.0", "react-test-renderer": "^16.0.0", @@ -132,12 +133,13 @@ "lru-cache": "^4.1.1", "mousetrap": "^1.6.1", "prop-types": "^15.5.10", + "react-dragtastic": "^2.4.3", "redux": "^4.0.0", "socket.io": "^2.1.1", "uuid": "3.2.1" }, "peerDependencies": { - "react": "^16.0.0" + "react": "^16.3.0" }, "config": { "exportAliases": { diff --git a/packages/ui.js b/packages/ui.js index 183a8eb1d..efb61b616 100644 --- a/packages/ui.js +++ b/packages/ui.js @@ -6,6 +6,7 @@ * https://opensource.org/licenses/MIT. */ +import { UI } from '../src/ui/ui.js'; import { Card } from '../src/ui/card.js'; import { Deck } from '../src/ui/deck.js'; import { Grid } from '../src/ui/grid.js'; @@ -13,4 +14,4 @@ import { HexGrid } from '../src/ui/hex.js'; import { HexUtils } from '../src/ui/hex-utils.js'; import Token from '../src/ui/token.js'; -export { Card, Deck, Grid, HexGrid, Token, HexUtils }; +export { UI, Card, Deck, Grid, HexGrid, Token, HexUtils }; diff --git a/rollup.npm.js b/rollup.npm.js index 4b45c6488..31b4077bc 100644 --- a/rollup.npm.js +++ b/rollup.npm.js @@ -29,6 +29,7 @@ const globals = { react: 'React', redux: 'Redux', 'prop-types': 'PropTypes', + 'react-dragtastic': 'ReactDragtastic', mousetrap: 'Mousetrap', 'socket.io-client': 'io', flatted: 'Flatted', diff --git a/src/ui/card.css b/src/ui/card.css index 1d2d5eb7c..44c49fa53 100644 --- a/src/ui/card.css +++ b/src/ui/card.css @@ -22,12 +22,22 @@ border: 1px solid #cdcdcd; width: 100px; height: 140px; - transition: all 0.1s; overflow: hidden; + transition: transform 0.1s; } -.bgio-card:not(.no-hover):hover { - transform: scale(1.2); +.bgio-card.placeholder { + cursor: default; + opacity: 0; + pointer-events: none; +} + +.bgio-card.accept { + transform: rotate(10deg); + box-shadow: 5px 5px 5px #ddd; +} + +.bgio-card.reject { } .bgio-card__front, diff --git a/src/ui/card.js b/src/ui/card.js index 8834d04e3..66108445c 100644 --- a/src/ui/card.js +++ b/src/ui/card.js @@ -9,37 +9,157 @@ import React from 'react'; import PropTypes from 'prop-types'; import Logo from './logo'; +import UIContext from './ui-context'; +import { Draggable, DragComponent } from 'react-dragtastic'; import './card.css'; -const Card = ({ back, canHover, className, front, isFaceUp, ...rest }) => { - const classNames = ['bgio-card']; - if (!canHover) classNames.push('no-hover'); - if (className) classNames.push(className); - - return ( -
- {isFaceUp ? front : back} -
- ); -}; - -Card.propTypes = { - back: PropTypes.node, - canHover: PropTypes.bool, - className: PropTypes.string, - front: PropTypes.node, - isFaceUp: PropTypes.bool, -}; - -Card.defaultProps = { - back: ( -
- -
- ), - canHover: true, - front:
Card
, - isFaceUp: false, -}; +/* eslint-disable */ +export function GetDraggable(props, classNames, cardStyle, onClick) { + return ({ isActive, events }) => { + return ( +
+ {props.isFaceUp ? props.front : props.back} +
+ ); + }; +} + +export function GetDragComponent( + props, + classNames, + ref, + isOverAcceptedCallback +) { + return ({ x, y, isOverAccepted, currentlyHoveredDroppableId }) => { + const classes = [...classNames]; + let content = props.back; + + isOverAcceptedCallback(isOverAccepted); + + if (props.isFaceUp) { + content = props.front; + } + + if (currentlyHoveredDroppableId !== null) { + if (isOverAccepted) { + classes.push('accept'); + } else { + classes.push('reject'); + } + } + + return ( +
+ {content} +
+ ); + }; +} +/* eslint-enable */ + +export class CardImpl extends React.Component { + static propTypes = { + isFaceUp: PropTypes.bool, + front: PropTypes.node, + back: PropTypes.node, + className: PropTypes.string, + dragZone: PropTypes.string, + style: PropTypes.any, + onClick: PropTypes.func, + context: PropTypes.any.isRequired, + inDeck: PropTypes.bool, + data: PropTypes.any, + deckPosition: PropTypes.number, + }; + + static defaultProps = { + onClick: () => {}, + isFaceUp: false, + dragZone: 'bgio-card', + front:
Card
, + back: ( +
+ +
+ ), + }; + + constructor(props) { + super(props); + this.id = props.context.genID(); + this.dragComponentRef = React.createRef(); + this.isOverAccepted = false; + } + + onClick = () => { + this.props.onClick(this.props.data); + }; + + render() { + const classNames = ['bgio-card']; + if (this.props.className) { + classNames.push(this.props.className); + } + + let cardStyle = {}; + + if (this.props.inDeck) { + cardStyle = { + position: 'absolute', + zIndex: this.props.deckPosition, + }; + } + + return ( +
+ + {GetDraggable(this.props, classNames, cardStyle, this.onClick)} + + + + {GetDragComponent( + this.props, + classNames, + this.dragComponentRef, + o => (this.isOverAccepted = o) + )} + +
+ ); + } +} + +const Card = props => ( + + {context => } + +); export { Card }; diff --git a/src/ui/card.test.js b/src/ui/card.test.js index f0d87aecb..36149e7e3 100644 --- a/src/ui/card.test.js +++ b/src/ui/card.test.js @@ -7,38 +7,126 @@ */ import React from 'react'; -import { Card } from './card'; +import { UI } from './ui'; +import { Card, CardImpl, GetDraggable, GetDragComponent } from './card'; import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new Adapter() }); -test('is rendered', () => { - { - const card = Enzyme.shallow(); - expect(card.html()).toBe( - '
Card
' +const context = { genID: () => 0 }; + +describe('Card', () => { + test('is rendered', () => { + const card = Enzyme.shallow( + + + + ); + expect(card.html()).toContain('
+ ); - } + expect(card.html()).toContain('custom'); + }); + + describe('onClick', () => { + test('default', () => { + const root = Enzyme.mount(); + root.instance().onClick(); + }); + + test('passed in', () => { + const onClick = jest.fn(); + const root = Enzyme.mount( + + ); + root.instance().onClick(); + expect(onClick).toHaveBeenCalled(); + }); + }); +}); + +describe('GetDraggable', () => { + test('is rendered', () => { + const Draggable = GetDraggable({}, [], {}); + const root = Enzyme.mount(); + expect(root.html()).toContain('div'); + }); - { - const card = Enzyme.shallow(); - expect(card.html()).toBe( - '
Card
' + test('is rendered - isActive', () => { + const Draggable = GetDraggable({}, [], {}); + const root = Enzyme.mount(); + expect(root.html()).toContain('div'); + }); + + test('isFaceUp', () => { + const Draggable = GetDraggable( + { isFaceUp: true, front: 'front-content' }, + [], + {} ); - } + const root = Enzyme.mount(); + expect(root.html()).toContain('front-content'); + }); }); -test('handlers', () => { - const onMouseOver = jest.fn(); - const onClick = jest.fn(); - const card = Enzyme.mount( - - ); +describe('GetDragComponent', () => { + let callback = jest.fn(); + + test('is rendered', () => { + const DragComponent = GetDragComponent({}, [], React.createRef(), callback); + const root = Enzyme.mount(); + expect(root.html()).toContain('div'); + }); + + test('isOverAccepted callback', () => { + const DragComponent = GetDragComponent({}, [], React.createRef(), callback); + const root = Enzyme.mount( + + ); + expect(root.html()).toContain('accept'); + expect(callback).toBeCalledWith(true); + }); - card.simulate('mouseover'); - card.simulate('click'); + test('isFaceUp', () => { + const DragComponent = GetDragComponent( + { isFaceUp: true, front: 'front-content' }, + [], + React.createRef(), + callback + ); + const root = Enzyme.mount( + + ); + expect(root.html()).toContain('front-content'); + }); +}); - expect(onMouseOver).toHaveBeenCalled(); - expect(onClick).toHaveBeenCalled(); +describe('DragComponent', () => { + test('isOverAccepted callback', () => { + const root = Enzyme.mount(); + const t = root.find('DragComponent'); + t.instance().props.children({ + x: 0, + y: 0, + isOverAccepted: true, + currentlyHoveredDroppableId: null, + }); + }); }); diff --git a/src/ui/deck.css b/src/ui/deck.css index d666a8f0f..9c43ff8be 100644 --- a/src/ui/deck.css +++ b/src/ui/deck.css @@ -7,7 +7,14 @@ */ .bgio-deck { + border: 1px dashed #ddd; position: relative; display: inline-flex; - z-index: 1; + border-radius: 6px; + padding: 5px; + margin-top: 20px; + margin-bottom: 20px; + margin-right: 20px; + width: 100px; + height: 140px; } diff --git a/src/ui/deck.js b/src/ui/deck.js index 5dc4fcef6..d19ce7256 100644 --- a/src/ui/deck.js +++ b/src/ui/deck.js @@ -8,71 +8,103 @@ import React from 'react'; import PropTypes from 'prop-types'; +import UIContext from './ui-context'; +import { Droppable } from 'react-dragtastic'; import './deck.css'; -class Deck extends React.Component { +export class DeckImpl extends React.Component { + static propTypes = { + context: PropTypes.any, + children: PropTypes.any, + onClick: PropTypes.func, + onDrop: PropTypes.func, + splayWidth: PropTypes.number, + dragZone: PropTypes.string, + padding: PropTypes.number, + className: PropTypes.string, + }; + + static defaultProps = { + padding: 10, + splayWidth: 3, + dragZone: 'bgio-card', + onDrop: () => {}, + onClick: () => {}, + }; + constructor(props) { super(props); - - this.state = { - cards: props.cards, - }; + this.id = props.context.genID(); } onClick = () => { - const cards = [...this.state.cards]; - const topCard = cards.shift(); + const cards = React.Children.toArray(this.props.children); + let topCardProps = null; - if (this.props.onClick) { - this.props.onClick(topCard); + if (cards.length > 0) { + topCardProps = cards[cards.length - 1].props; + this.props.onClick(topCardProps.data); } + }; - this.setState({ - cards, + onDrop = cardData => { + // Don't fire onDrop if the top card of this deck was + // dragged away and then dropped back. + let isChild = false; + React.Children.forEach(this.props.children, card => { + if (cardData !== undefined && card.props.data === cardData) { + isChild = true; + } }); - }; - componentWillReceiveProps(nextProps) { - if (this.props.cards.length !== nextProps.cards.length) { - this.setState({ cards: nextProps.cards }); + if (!isChild) { + this.props.onDrop(cardData); } - } + }; render() { - const { className, splayWidth, ...rest } = this.props; - const { cards } = this.state; - const classNames = ['bgio-deck']; - if (className) classNames.push(className); + let cardIndex = 0; + const cards = React.Children.map(this.props.children, card => + React.cloneElement(card, { + dragZone: this.props.dragZone, + inDeck: true, + deckPosition: cardIndex++, + }) + ); return ( -
- {cards.map((card, i) => - React.cloneElement(card, { - key: i, - canHover: i === 0, // Only the top card should apply a css hover effect - isFaceUp: i === 0, // Only the top card should ever be face up - style: { - position: i ? 'absolute' : 'inherit', - left: i * splayWidth, - zIndex: -i, - }, - }) - )} +
+ + {({ events }) => { + return ( +
+ {cards} +
+ ); + }} +
); } } -Deck.propTypes = { - cards: PropTypes.arrayOf(PropTypes.node), - className: PropTypes.string, - onClick: PropTypes.func, - splayWidth: PropTypes.number, -}; - -Deck.defaultProps = { - cards: [], - splayWidth: 3, -}; +const Deck = props => ( + + {context => } + +); export { Deck }; diff --git a/src/ui/deck.test.js b/src/ui/deck.test.js index 1c4541803..7c20a83ec 100644 --- a/src/ui/deck.test.js +++ b/src/ui/deck.test.js @@ -7,55 +7,116 @@ */ import React from 'react'; +import { UI } from './ui'; import { Card } from './card'; -import { Deck } from './deck'; +import { Deck, DeckImpl } from './deck'; import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; Enzyme.configure({ adapter: new Adapter() }); -test('basic', () => { - const cards = [, ]; +const context = { genID: () => 0 }; - { - const deck = Enzyme.shallow(); - expect(deck.html()).toContain('svg'); - } +describe('basic', () => { + test('cards are rendered', () => { + const deck = Enzyme.shallow( + + + + + + + ); + expect(deck.find(Card).length).toBe(2); + }); - { - const deck = Enzyme.shallow(); + test('custom class', () => { + const deck = Enzyme.shallow( + + ); expect(deck.html()).toContain('custom'); - } + }); }); -test('onClick', () => { - const cards = [, ]; +describe('onClick', () => { + test('onClick not passed', () => { + const root = Enzyme.mount( + + + + + + + ); + root + .find('DeckImpl') + .instance() + .onClick(); + }); - { - const deck = Enzyme.mount(); - expect(deck.state().cards.length).toBe(2); - deck.simulate('click'); - expect(deck.state().cards.length).toBe(1); - } + test('calls props.onClick - no top card', () => { + const onClick = jest.fn(); + const deck = Enzyme.mount(); + deck.instance().onClick(); + expect(onClick).not.toHaveBeenCalled(); + }); - { + test('calls props.onClick - top card', () => { const onClick = jest.fn(); - const deck = Enzyme.mount(); - deck.simulate('click'); - expect(onClick).toHaveBeenCalled(); - } + const deck = Enzyme.mount( + + + + + + ); + deck + .find('DeckImpl') + .instance() + .onClick(); + expect(onClick).toHaveBeenCalledWith('data'); + }); }); -test('update cards prop', () => { - const oldCards = [, ]; - const newCards = []; +describe('onDrop', () => { + let root; + let onDrop; + + beforeEach(() => { + onDrop = jest.fn(); + root = Enzyme.mount( + + + + + + ); + }); - const deck = Enzyme.mount(); - expect(deck.state().cards.length).toBe(2); - deck.setProps({ cards: newCards }); - expect(deck.state().cards.length).toBe(1); - deck.setProps({ cards: newCards }); - expect(deck.state().cards.length).toBe(1); + test('onDrop not passed', () => { + const deck = Enzyme.mount(); + deck.instance().onDrop(); + }); + + test('calls props.onDrop', () => { + const onDrop = jest.fn(); + const deck = Enzyme.mount(); + deck.instance().onDrop({ id: '0' }); + expect(onDrop).toHaveBeenCalled(); + }); + + test('but not if dropped on same deck', () => { + const cardData = root.find('CardImpl').instance().props.data; + const deck = root.find('DeckImpl').instance(); + deck.onDrop(cardData); + expect(onDrop).not.toHaveBeenCalled(); + }); + + test('cardData undefined', () => { + const deck = root.find('DeckImpl').instance(); + deck.onDrop(); + expect(onDrop).toHaveBeenCalled(); + }); }); test('splayWidth', () => { @@ -66,7 +127,11 @@ test('splayWidth', () => { , ]; const splayWidth = 10; - const deck = Enzyme.mount(); + const deck = Enzyme.shallow( + + {cards} + + ); deck.find('.bgio-card').forEach((node, index) => { expect(node.props().style.left).toEqual(splayWidth * index); diff --git a/src/ui/ui-context.js b/src/ui/ui-context.js new file mode 100644 index 000000000..b01122dbe --- /dev/null +++ b/src/ui/ui-context.js @@ -0,0 +1,13 @@ +/* + * Copyright 2018 The boardgame.io Authors + * + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import React from 'react'; + +const UIContext = React.createContext(); + +export default UIContext; diff --git a/src/ui/ui.js b/src/ui/ui.js new file mode 100644 index 000000000..02808a35c --- /dev/null +++ b/src/ui/ui.js @@ -0,0 +1,41 @@ +/* + * Copyright 2018 The boardgame.io Authors + * + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import UIContext from './ui-context'; + +/** + * Root element of the UI framework. + */ +class UI extends React.Component { + static propTypes = { + children: PropTypes.any, + }; + + constructor(props) { + super(props); + this._nextID = 1; + } + + getContext = () => { + return { + genID: () => this._nextID++, + }; + }; + + render() { + return ( + +
{this.props.children}
+
+ ); + } +} + +export { UI }; diff --git a/src/ui/ui.ssr.test.js b/src/ui/ui.ssr.test.js new file mode 100644 index 000000000..1318a8147 --- /dev/null +++ b/src/ui/ui.ssr.test.js @@ -0,0 +1,17 @@ +/** + * @jest-environment node + */ + +import React from 'react'; +import { UI } from './ui'; +import { Card } from './card'; +import ReactDOMServer from 'react-dom/server'; + +test('SSR', () => { + let ssrRender = ReactDOMServer.renderToString( + + + + ); + expect(ssrRender).toContain('bgio-card'); +}); diff --git a/src/ui/ui.test.js b/src/ui/ui.test.js new file mode 100644 index 000000000..d99136f3c --- /dev/null +++ b/src/ui/ui.test.js @@ -0,0 +1,44 @@ +/* + * Copyright 2018 The boardgame.io Authors + * + * Use of this source code is governed by a MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ + +import React from 'react'; +import Enzyme from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { Card } from './card'; +import { Deck } from './deck'; +import { UI } from './ui'; + +Enzyme.configure({ adapter: new Adapter() }); + +beforeEach(() => { + jest.resetModules(); +}); + +describe('basic', () => { + let root; + beforeEach(() => { + root = Enzyme.mount( + +
+ + + +
+ +
+ + + + ); + }); + + test('is rendered', () => { + expect(root.find(Deck).length).toBe(1); + expect(root.find(Card).length).toBe(2); + }); +}); diff --git a/storybook/DeckStories.js b/storybook/DeckStories.js index b5eb886ab..6676a2efb 100644 --- a/storybook/DeckStories.js +++ b/storybook/DeckStories.js @@ -30,7 +30,9 @@ export class StandardDeckStory extends React.Component { onClick = card => { const selectedCard = card.props; action('onClick')(JSON.stringify(selectedCard.card)); - this.setState({ selectedCard }); + let deck = this.state.deck; + deck.shift(); + this.setState({ selectedCard, deck }); }; renderCard = card => ( @@ -45,7 +47,7 @@ export class StandardDeckStory extends React.Component { return (
- + {deck.map(this.renderCard)} {this.state.selectedCard && (