diff --git a/README.md b/README.md
index 5261d574e..d72e0a1f5 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[![Travis Status][trav_img]][trav_site]
ReactJS based Presentation Library
-[Spectacle Boilerplate MDX](https://github.com/FormidableLabs/spectacle-boilerplate-mdx/)
+[Spectacle Boilerplate MDX](https://github.com/FormidableLabs/spectacle-boilerplate-mdx/)
[Spectacle Boilerplate](https://github.com/FormidableLabs/spectacle-boilerplate/)
Have a question about Spectacle? Submit an issue in this repository using the "Question" template.
@@ -465,20 +465,21 @@ In Spectacle, presentations are composed of a set of base tags. We can separate
The Deck tag is the root level tag for your presentation. It supports the following props:
-| Name | PropType | Description | Default |
-| ----------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
-| autoplay | PropTypes.bool | Automatically advance slides. | `false` |
-| autoplayDuration | PropTypes.number | Accepts integer value in milliseconds for global autoplay duration. | `7000` |
-| autoplayLoop | PropTypes.bool | Keep slides in loop. | `true` |
-| controls | PropTypes.bool | Show control arrows when not in fullscreen. | `true` |
-| contentHeight | PropTypes.numbers | Baseline content area height. | `700px` |
-| contentWidth | PropTypes.numbers | Baseline content area width. | `1000px` |
-| disableKeyboardControls | PropTypes.bool | Toggle keyboard control. | `false` |
-| history | PropTypes.object | Accepts custom configuration for [history](https://github.com/ReactTraining/history). | |
-| progress | PropTypes.string | Accepts `pacman`, `bar`, `number` or `none`. To override the color, change the 'quaternary' color in the theme. | `pacman` |
-| theme | PropTypes.object | Accepts a theme object for styling your presentation. | |
-| transition | PropTypes.array | Accepts `slide`, `zoom`, `fade` or `spin`, and can be combined. Sets global slide transitions. **Note: If you use the 'scale' transition, fitted text won't work in Safari.** | |
-| transitionDuration | PropTypes.number | Accepts integer value in milliseconds for global transition duration. | `500` |
+| Name | PropType | Description | Default |
+| ----------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
+| autoplay | PropTypes.bool | Automatically advance slides. | `false` |
+| autoplayDuration | PropTypes.number | Accepts integer value in milliseconds for global autoplay duration. | `7000` |
+| autoplayLoop | PropTypes.bool | Keep slides in loop. | `true` |
+| controls | PropTypes.bool | Show control arrows when not in fullscreen. | `true` |
+| contentHeight | PropTypes.numbers | Baseline content area height. | `700px` |
+| contentWidth | PropTypes.numbers | Baseline content area width. | `1000px` |
+| disableKeyboardControls | PropTypes.bool | Toggle keyboard control. | `false` |
+| onStateChange | PropTypes.func | Called whenever a new slide becomes visible with the arguments `(previousState, nextState)` where state refers to the outgoing and incoming ``'s `state` props, respectively. The default implementation attaches the current state as a class to the document root. | see description |
+| history | PropTypes.object | Accepts custom configuration for [history](https://github.com/ReactTraining/history). | |
+| progress | PropTypes.string | Accepts `pacman`, `bar`, `number` or `none`. To override the color, change the 'quaternary' color in the theme. | `pacman` |
+| theme | PropTypes.object | Accepts a theme object for styling your presentation. | |
+| transition | PropTypes.array | Accepts `slide`, `zoom`, `fade` or `spin`, and can be combined. Sets global slide transitions. **Note: If you use the 'scale' transition, fitted text won't work in Safari.** | |
+| transitionDuration | PropTypes.number | Accepts integer value in milliseconds for global transition duration. | `500` |
@@ -497,6 +498,7 @@ The slide tag represents each slide in the presentation. Giving a slide tag an `
| notes | PropTypes.string | Text which will appear in the presenter mode. Can be HTML. | |
| onActive | PropTypes.func | Optional function that is called with the slide index when the slide comes into view. | |
| progressColor | PropTypes.string | Used to override color of progress elements on a per slide basis, accepts color aliases, or valid color values. | `quaternary` color set by theme |
+| state | PropTypes.string | Used to indicate that the deck is in a specific state. Inspired by [Reveal.js](https://github.com/hakimel/reveal.js)'s `data-state` attribute | |
| transition | PropTypes.array | Used to override transition prop on a per slide basis, accepts `slide`, `zoom`, `fade`, `spin`, or a [function](#transition-function), and can be combined. This will affect both enter and exit transitions. **Note: If you use the 'scale' transition, fitted text won't work in Safari.** | Set by `Deck`'s `transition` prop |
| transitionIn | PropTypes.array | Specifies the slide transition when the slide comes into view. Accepts the same values as transition. |
| transitionOut | PropTypes.array | Specifies the slide transition when the slide exits. Accepts the same values as transition. | Set by `Deck`'s `transition` prop |
diff --git a/src/components/deck.js b/src/components/deck.js
index b991f95bf..fb9108f36 100644
--- a/src/components/deck.js
+++ b/src/components/deck.js
@@ -9,6 +9,16 @@ import Manager from './manager';
const store = configureStore();
+export function defaultOnStateChange(prevState, nextState) {
+ if (nextState) {
+ document.documentElement.classList.add(nextState);
+ }
+
+ if (prevState) {
+ document.documentElement.classList.remove(prevState);
+ }
+}
+
export default class Deck extends Component {
static displayName = 'Deck';
@@ -20,12 +30,36 @@ export default class Deck extends Component {
controls: PropTypes.bool,
globalStyles: PropTypes.bool,
history: PropTypes.object,
+ onStateChange: PropTypes.func,
progress: PropTypes.oneOf(['pacman', 'bar', 'number', 'none']),
theme: PropTypes.object,
transition: PropTypes.array,
transitionDuration: PropTypes.number
};
+ static defaultProps = {
+ onStateChange: defaultOnStateChange
+ };
+
+ state = {
+ slideState: undefined
+ };
+
+ componentWillUnmount() {
+ // Cleanup default onStateChange
+ if (this.state.slideState && !this.props.onStateChange) {
+ document.documentElement.classList.remove(this.state.slideState);
+ }
+ }
+
+ handleStateChange = nextState => {
+ const prevState = this.state.slideState;
+ if (prevState !== nextState) {
+ this.props.onStateChange(prevState, nextState);
+ this.setState({ slideState: nextState });
+ }
+ };
+
render() {
return (
@@ -33,6 +67,7 @@ export default class Deck extends Component {
theme={this.props.theme}
store={store}
history={this.props.history}
+ onStateChange={this.handleStateChange}
>
{this.props.children}
diff --git a/src/components/deck.test.js b/src/components/deck.test.js
new file mode 100644
index 000000000..ac2971b47
--- /dev/null
+++ b/src/components/deck.test.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import Deck, { defaultOnStateChange } from './deck';
+import Slide from './slide';
+import Text from './text';
+
+describe('', () => {
+ let wrapper;
+
+ afterEach(() => {
+ if (wrapper && wrapper.unmount) {
+ wrapper.unmount();
+ wrapper = null;
+ }
+ });
+
+ test('should call onStateChange prop, with s state prop', () => {
+ const onStateChangeSpy = jest.fn();
+
+ wrapper = mount(
+
+
+ Test slide
+
+
+ Test slide
+
+
+ );
+
+ expect(onStateChangeSpy).toHaveBeenCalledTimes(1);
+ expect(onStateChangeSpy).lastCalledWith(
+ undefined, // previous state
+ 'slide-1' // next state
+ );
+ });
+
+ test('should call onStateChange prop, with previous state and s state prop as the next state', () => {
+ const onStateChangeSpy = jest.fn();
+
+ wrapper = mount(
+
+
+ Test slide
+
+
+ Test slide
+
+
+ );
+
+ expect(onStateChangeSpy).lastCalledWith(
+ undefined, // previous state
+ 'slide-1' // next state
+ );
+
+ // Go to the next slide
+ wrapper.find('[aria-label="Next slide"]').simulate('click');
+
+ expect(onStateChangeSpy).toHaveBeenCalledTimes(2);
+ expect(onStateChangeSpy).lastCalledWith(
+ 'slide-1', // previous state
+ 'slide-2' // next state
+ );
+ });
+
+ test('default onStateChange implementation adds the current state as a class to the document root', () => {
+ defaultOnStateChange(undefined, 'slide-1');
+ expect(document.documentElement.classList.contains('slide-1')).toBe(true);
+
+ defaultOnStateChange('slide-1', 'slide-2');
+ expect(document.documentElement.classList.contains('slide-1')).toBe(false);
+ expect(document.documentElement.classList.contains('slide-2')).toBe(true);
+ });
+});
diff --git a/src/components/slide.js b/src/components/slide.js
index ba28657d3..c83cce02b 100644
--- a/src/components/slide.js
+++ b/src/components/slide.js
@@ -62,6 +62,8 @@ class Slide extends React.PureComponent {
});
}
+ this.context.onStateChange(this.props.state);
+
if (isFunction(this.props.onActive)) {
this.props.onActive(this.props.slideIndex);
}
@@ -155,6 +157,7 @@ Slide.propTypes = {
print: PropTypes.bool,
slideIndex: PropTypes.number,
slideReference: PropTypes.array,
+ state: PropTypes.string,
style: PropTypes.object,
transition: PropTypes.array,
transitionDuration: PropTypes.number,
@@ -164,13 +167,14 @@ Slide.propTypes = {
};
Slide.contextTypes = {
- styles: PropTypes.object,
- contentWidth: PropTypes.number,
contentHeight: PropTypes.number,
+ contentWidth: PropTypes.number,
export: PropTypes.bool,
- print: PropTypes.object,
+ onStateChange: PropTypes.func.isRequired,
overview: PropTypes.bool,
- store: PropTypes.object
+ print: PropTypes.object,
+ store: PropTypes.object,
+ styles: PropTypes.object
};
Slide.childContextTypes = {
diff --git a/src/components/slide.test.js b/src/components/slide.test.js
index 236c98626..94443fa05 100644
--- a/src/components/slide.test.js
+++ b/src/components/slide.test.js
@@ -2,6 +2,7 @@ import React from 'react';
import { mount } from 'enzyme';
import Slide from './slide';
import Appear from './appear';
+import Text from './text';
const _mockContext = function() {
return {
@@ -19,7 +20,8 @@ const _mockContext = function() {
getState: () => ({ route: { params: '', slide: 0 } }),
subscribe: () => {},
dispatch: () => {}
- }
+ },
+ onStateChange: () => {}
};
};
@@ -173,4 +175,18 @@ describe('', () => {
]
]);
});
+
+ test.only('should call `onStateChange` on mount', () => {
+ const onStateChangeSpy = jest.fn();
+ const context = { ..._mockContext(), onStateChange: onStateChangeSpy };
+ mount(
+
+ Test slide
+ ,
+ { context }
+ );
+
+ expect(onStateChangeSpy).toHaveBeenCalledTimes(1);
+ expect(onStateChangeSpy).lastCalledWith('slide-1');
+ });
});
diff --git a/src/utils/context.js b/src/utils/context.js
index 989bb75ad..032666467 100644
--- a/src/utils/context.js
+++ b/src/utils/context.js
@@ -6,20 +6,23 @@ class Context extends Component {
static propTypes = {
children: PropTypes.node,
history: PropTypes.object,
+ onStateChange: PropTypes.func,
store: PropTypes.object,
styles: PropTypes.object
};
static childContextTypes = {
- styles: PropTypes.object,
history: PropTypes.object,
+ onStateChange: PropTypes.func,
+ styles: PropTypes.object,
store: PropTypes.object
};
getChildContext() {
- const { history, styles, store } = this.props;
+ const { history, onStateChange, styles, store } = this.props;
return {
history,
- styles,
- store
+ onStateChange,
+ store,
+ styles
};
}
render() {
diff --git a/src/utils/controller.js b/src/utils/controller.js
index ba07272e0..bb7609353 100644
--- a/src/utils/controller.js
+++ b/src/utils/controller.js
@@ -13,6 +13,7 @@ export default class Controller extends Component {
static propTypes = {
children: PropTypes.node,
history: PropTypes.object,
+ onStateChange: PropTypes.func.isRequired,
store: PropTypes.object,
theme: PropTypes.object
};
@@ -69,8 +70,9 @@ export default class Controller extends Component {
return (
{this.props.children}