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}