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.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 && (