Skip to content

Commit 58c12bd

Browse files
authored
Add slide state (#626)
* Add slide state * Don't remove `--verbose` from test script
1 parent 761908c commit 58c12bd

File tree

7 files changed

+162
-25
lines changed

7 files changed

+162
-25
lines changed

README.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Travis Status][trav_img]][trav_site]
44
ReactJS based Presentation Library
55

6-
[Spectacle Boilerplate MDX](https://github.com/FormidableLabs/spectacle-boilerplate-mdx/)
6+
[Spectacle Boilerplate MDX](https://github.com/FormidableLabs/spectacle-boilerplate-mdx/)
77
[Spectacle Boilerplate](https://github.com/FormidableLabs/spectacle-boilerplate/)
88

99
Have a question about Spectacle? Submit an issue in this repository using the "Question" template.
@@ -472,20 +472,21 @@ In Spectacle, presentations are composed of a set of base tags. We can separate
472472

473473
The Deck tag is the root level tag for your presentation. It supports the following props:
474474

475-
| Name | PropType | Description | Default |
476-
| ----------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
477-
| autoplay | PropTypes.bool | Automatically advance slides. | `false` |
478-
| autoplayDuration | PropTypes.number | Accepts integer value in milliseconds for global autoplay duration. | `7000` |
479-
| autoplayLoop | PropTypes.bool | Keep slides in loop. | `true` |
480-
| controls | PropTypes.bool | Show control arrows when not in fullscreen. | `true` |
481-
| contentHeight | PropTypes.numbers | Baseline content area height. | `700px` |
482-
| contentWidth | PropTypes.numbers | Baseline content area width. | `1000px` |
483-
| disableKeyboardControls | PropTypes.bool | Toggle keyboard control. | `false` |
484-
| history | PropTypes.object | Accepts custom configuration for [history](https://github.com/ReactTraining/history). | |
485-
| progress | PropTypes.string | Accepts `pacman`, `bar`, `number` or `none`. To override the color, change the 'quaternary' color in the theme. | `pacman` |
486-
| theme | PropTypes.object | Accepts a theme object for styling your presentation. | |
487-
| 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.** | |
488-
| transitionDuration | PropTypes.number | Accepts integer value in milliseconds for global transition duration. | `500` |
475+
| Name | PropType | Description | Default |
476+
| ----------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
477+
| autoplay | PropTypes.bool | Automatically advance slides. | `false` |
478+
| autoplayDuration | PropTypes.number | Accepts integer value in milliseconds for global autoplay duration. | `7000` |
479+
| autoplayLoop | PropTypes.bool | Keep slides in loop. | `true` |
480+
| controls | PropTypes.bool | Show control arrows when not in fullscreen. | `true` |
481+
| contentHeight | PropTypes.numbers | Baseline content area height. | `700px` |
482+
| contentWidth | PropTypes.numbers | Baseline content area width. | `1000px` |
483+
| disableKeyboardControls | PropTypes.bool | Toggle keyboard control. | `false` |
484+
| onStateChange | PropTypes.func | Called whenever a new slide becomes visible with the arguments `(previousState, nextState)` where state refers to the outgoing and incoming `<Slide />`'s `state` props, respectively. The default implementation attaches the current state as a class to the document root. | see description |
485+
| history | PropTypes.object | Accepts custom configuration for [history](https://github.com/ReactTraining/history). | |
486+
| progress | PropTypes.string | Accepts `pacman`, `bar`, `number` or `none`. To override the color, change the 'quaternary' color in the theme. | `pacman` |
487+
| theme | PropTypes.object | Accepts a theme object for styling your presentation. | |
488+
| 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.** | |
489+
| transitionDuration | PropTypes.number | Accepts integer value in milliseconds for global transition duration. | `500` |
489490

490491
<a name="slide-base"></a>
491492

@@ -504,6 +505,7 @@ The slide tag represents each slide in the presentation. Giving a slide tag an `
504505
| notes | PropTypes.string | Text which will appear in the presenter mode. Can be HTML. | |
505506
| onActive | PropTypes.func | Optional function that is called with the slide index when the slide comes into view. | |
506507
| 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 |
508+
| 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 | |
507509
| 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 |
508510
| transitionIn | PropTypes.array | Specifies the slide transition when the slide comes into view. Accepts the same values as transition. |
509511
| transitionOut | PropTypes.array | Specifies the slide transition when the slide exits. Accepts the same values as transition. | Set by `Deck`'s `transition` prop |

src/components/deck.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ import Manager from './manager';
99

1010
const store = configureStore();
1111

12+
export function defaultOnStateChange(prevState, nextState) {
13+
if (nextState) {
14+
document.documentElement.classList.add(nextState);
15+
}
16+
17+
if (prevState) {
18+
document.documentElement.classList.remove(prevState);
19+
}
20+
}
21+
1222
export default class Deck extends Component {
1323
static displayName = 'Deck';
1424

@@ -20,19 +30,44 @@ export default class Deck extends Component {
2030
controls: PropTypes.bool,
2131
globalStyles: PropTypes.bool,
2232
history: PropTypes.object,
33+
onStateChange: PropTypes.func,
2334
progress: PropTypes.oneOf(['pacman', 'bar', 'number', 'none']),
2435
theme: PropTypes.object,
2536
transition: PropTypes.array,
2637
transitionDuration: PropTypes.number
2738
};
2839

40+
static defaultProps = {
41+
onStateChange: defaultOnStateChange
42+
};
43+
44+
state = {
45+
slideState: undefined
46+
};
47+
48+
componentWillUnmount() {
49+
// Cleanup default onStateChange
50+
if (this.state.slideState && !this.props.onStateChange) {
51+
document.documentElement.classList.remove(this.state.slideState);
52+
}
53+
}
54+
55+
handleStateChange = nextState => {
56+
const prevState = this.state.slideState;
57+
if (prevState !== nextState) {
58+
this.props.onStateChange(prevState, nextState);
59+
this.setState({ slideState: nextState });
60+
}
61+
};
62+
2963
render() {
3064
return (
3165
<Provider store={store}>
3266
<Controller
3367
theme={this.props.theme}
3468
store={store}
3569
history={this.props.history}
70+
onStateChange={this.handleStateChange}
3671
>
3772
<Manager {...this.props}>{this.props.children}</Manager>
3873
</Controller>

src/components/deck.test.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React from 'react';
2+
import { mount } from 'enzyme';
3+
import Deck, { defaultOnStateChange } from './deck';
4+
import Slide from './slide';
5+
import Text from './text';
6+
7+
describe('<Deck />', () => {
8+
let wrapper;
9+
10+
afterEach(() => {
11+
if (wrapper && wrapper.unmount) {
12+
wrapper.unmount();
13+
wrapper = null;
14+
}
15+
});
16+
17+
test('should call onStateChange prop, with <Slide />s state prop', () => {
18+
const onStateChangeSpy = jest.fn();
19+
20+
wrapper = mount(
21+
<Deck onStateChange={onStateChangeSpy}>
22+
<Slide state="slide-1">
23+
<Text>Test slide</Text>
24+
</Slide>
25+
<Slide state="slide-2">
26+
<Text>Test slide</Text>
27+
</Slide>
28+
</Deck>
29+
);
30+
31+
expect(onStateChangeSpy).toHaveBeenCalledTimes(1);
32+
expect(onStateChangeSpy).lastCalledWith(
33+
undefined, // previous state
34+
'slide-1' // next state
35+
);
36+
});
37+
38+
test('should call onStateChange prop, with previous state and <Slide />s state prop as the next state', () => {
39+
const onStateChangeSpy = jest.fn();
40+
41+
wrapper = mount(
42+
<Deck onStateChange={onStateChangeSpy}>
43+
<Slide state="slide-1">
44+
<Text>Test slide</Text>
45+
</Slide>
46+
<Slide state="slide-2">
47+
<Text>Test slide</Text>
48+
</Slide>
49+
</Deck>
50+
);
51+
52+
expect(onStateChangeSpy).lastCalledWith(
53+
undefined, // previous state
54+
'slide-1' // next state
55+
);
56+
57+
// Go to the next slide
58+
wrapper.find('[aria-label="Next slide"]').simulate('click');
59+
60+
expect(onStateChangeSpy).toHaveBeenCalledTimes(2);
61+
expect(onStateChangeSpy).lastCalledWith(
62+
'slide-1', // previous state
63+
'slide-2' // next state
64+
);
65+
});
66+
67+
test('default onStateChange implementation adds the current state as a class to the document root', () => {
68+
defaultOnStateChange(undefined, 'slide-1');
69+
expect(document.documentElement.classList.contains('slide-1')).toBe(true);
70+
71+
defaultOnStateChange('slide-1', 'slide-2');
72+
expect(document.documentElement.classList.contains('slide-1')).toBe(false);
73+
expect(document.documentElement.classList.contains('slide-2')).toBe(true);
74+
});
75+
});

src/components/slide.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class Slide extends React.PureComponent {
6262
});
6363
}
6464

65+
this.context.onStateChange(this.props.state);
66+
6567
if (isFunction(this.props.onActive)) {
6668
this.props.onActive(this.props.slideIndex);
6769
}
@@ -155,6 +157,7 @@ Slide.propTypes = {
155157
print: PropTypes.bool,
156158
slideIndex: PropTypes.number,
157159
slideReference: PropTypes.array,
160+
state: PropTypes.string,
158161
style: PropTypes.object,
159162
transition: PropTypes.array,
160163
transitionDuration: PropTypes.number,
@@ -164,13 +167,14 @@ Slide.propTypes = {
164167
};
165168

166169
Slide.contextTypes = {
167-
styles: PropTypes.object,
168-
contentWidth: PropTypes.number,
169170
contentHeight: PropTypes.number,
171+
contentWidth: PropTypes.number,
170172
export: PropTypes.bool,
171-
print: PropTypes.object,
173+
onStateChange: PropTypes.func.isRequired,
172174
overview: PropTypes.bool,
173-
store: PropTypes.object
175+
print: PropTypes.object,
176+
store: PropTypes.object,
177+
styles: PropTypes.object
174178
};
175179

176180
Slide.childContextTypes = {

src/components/slide.test.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { mount } from 'enzyme';
33
import Slide from './slide';
44
import Appear from './appear';
5+
import Text from './text';
56

67
const _mockContext = function() {
78
return {
@@ -19,7 +20,8 @@ const _mockContext = function() {
1920
getState: () => ({ route: { params: '', slide: 0 } }),
2021
subscribe: () => {},
2122
dispatch: () => {}
22-
}
23+
},
24+
onStateChange: () => {}
2325
};
2426
};
2527

@@ -173,4 +175,18 @@ describe('<Slide />', () => {
173175
]
174176
]);
175177
});
178+
179+
test.only('should call `onStateChange` on mount', () => {
180+
const onStateChangeSpy = jest.fn();
181+
const context = { ..._mockContext(), onStateChange: onStateChangeSpy };
182+
mount(
183+
<Slide state="slide-1">
184+
<Text>Test slide</Text>
185+
</Slide>,
186+
{ context }
187+
);
188+
189+
expect(onStateChangeSpy).toHaveBeenCalledTimes(1);
190+
expect(onStateChangeSpy).lastCalledWith('slide-1');
191+
});
176192
});

src/utils/context.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,23 @@ class Context extends Component {
66
static propTypes = {
77
children: PropTypes.node,
88
history: PropTypes.object,
9+
onStateChange: PropTypes.func,
910
store: PropTypes.object,
1011
styles: PropTypes.object
1112
};
1213
static childContextTypes = {
13-
styles: PropTypes.object,
1414
history: PropTypes.object,
15+
onStateChange: PropTypes.func,
16+
styles: PropTypes.object,
1517
store: PropTypes.object
1618
};
1719
getChildContext() {
18-
const { history, styles, store } = this.props;
20+
const { history, onStateChange, styles, store } = this.props;
1921
return {
2022
history,
21-
styles,
22-
store
23+
onStateChange,
24+
store,
25+
styles
2326
};
2427
}
2528
render() {

src/utils/controller.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export default class Controller extends Component {
1313
static propTypes = {
1414
children: PropTypes.node,
1515
history: PropTypes.object,
16+
onStateChange: PropTypes.func.isRequired,
1617
store: PropTypes.object,
1718
theme: PropTypes.object
1819
};
@@ -69,8 +70,9 @@ export default class Controller extends Component {
6970

7071
return (
7172
<Context
72-
store={this.props.store}
7373
history={this.history}
74+
onStateChange={this.props.onStateChange}
75+
store={this.props.store}
7476
styles={this.state.print ? styles.print : styles.screen}
7577
>
7678
{this.props.children}

0 commit comments

Comments
 (0)