1
1
mirror of https://github.com/jxnblk/mdx-deck.git synced 2024-11-26 00:35:02 +03:00

Merge pull request #337 from jxnblk/refactor-modes

Refactor to use hooks
This commit is contained in:
Brent Jackson 2019-04-20 19:32:53 -04:00 committed by GitHub
commit c967aab936
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 349 additions and 244 deletions

View File

@ -5,6 +5,11 @@
- Refactor localStorage to use hooks #334
- Refactor keyboard shortcuts #335
- Refactor query string to use hooks #336
- Refactor to use hooks #337
- Adds `MDXDeckState` provider component
- Fixes an issue with rerenders in Gatsby theme
- Adjusts styles in grid mode
- Refactors `useSteps` to use effect hook
## v2.2.3 2019-04-20

View File

@ -57,8 +57,10 @@ import { Box } from '@rebass/emotion'
```
<Notes>
These are speaker notes
- These are speaker notes
- And they won't be rendered in your slide
</Notes>
---

View File

@ -10,6 +10,9 @@
"start": "yarn workspace @mdx-deck/docs start",
"analyze-bundle": "yarn workspace @mdx-deck/docs start --webpack bundle-analyzer.config.js",
"build": "yarn workspace @mdx-deck/docs build",
"start-theme": "yarn workspace @mdx-deck/gatsby-theme start",
"build-theme": "yarn workspace @mdx-deck/gatsby-theme build",
"export": "yarn workspace @mdx-deck/export pdf",
"test": "jest"
},
"devDependencies": {

View File

@ -21,6 +21,7 @@
"devDependencies": {
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-test-renderer": "^16.8.4"
"react-test-renderer": "^16.8.4",
"react-testing-library": "^6.1.2"
}
}

View File

@ -1,22 +1,9 @@
import React, { useEffect } from 'react'
import { Location, navigate } from '@reach/router'
import Zoom from './Zoom'
import Slide from './Slide'
const getIndex = ({ pathname }) => {
return Number(pathname.split('/')[1] || 0)
}
const withLocation = Component => props => (
<Location
children={({ location }) => (
<Component {...props} location={location} index={getIndex(location)} />
)}
/>
)
export const Grid = withLocation(props => {
const { index, slides, modes, update, basepath } = props
export const Grid = props => {
const { index, slides, modes, update, goto } = props
const activeThumb = React.createRef()
useEffect(() => {
@ -49,18 +36,19 @@ export const Grid = withLocation(props => {
key={i}
role="link"
onClick={e => {
navigate(basepath + '/' + i)
goto(i)
update({ mode: modes.NORMAL })
}}
style={{
display: 'block',
width: '25vw',
height: '25vh',
padding: '2px',
width: 'calc(25vw - 4px)',
height: 'calc(25vh - 4px)',
margin: '2px',
overflow: 'hidden',
color: 'inherit',
textDecoration: 'none',
cursor: 'pointer',
outline: i === index ? '4px solid #0cf' : null,
}}
>
<Zoom zoom={1 / 4}>
@ -73,6 +61,6 @@ export const Grid = withLocation(props => {
</div>
</div>
)
})
}
export default Grid

View File

@ -20,6 +20,7 @@ export const HeadProvider = ({ tags = [], children }) => {
// get head for all slides
export const UserHead = ({ mdx }) =>
!!mdx &&
React.createElement(mdx, {
components: {
wrapper: props => {

View File

@ -69,21 +69,13 @@ const handleKeyDown = props => e => {
}
}
export const Keyboard = props => {
export default props => {
useEffect(() => {
const handler = handleKeyDown(props)
window.addEventListener('keydown', handler)
return () => {
window.removeEventListener('keydown', handler)
}
}, [])
}, [props.metadata])
return false
}
const noop = () => {}
Keyboard.defaultProps = {
setState: noop,
}
export default Keyboard

View File

@ -1,8 +1,10 @@
import React from 'react'
import React, { useContext, useReducer, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Router, globalHistory, navigate } from '@reach/router'
import { Global } from '@emotion/core'
import { Swipeable } from 'react-swipeable'
import merge from 'lodash.merge'
import Provider from './Provider'
import Slide from './Slide'
import Presenter from './Presenter'
@ -11,9 +13,10 @@ import Grid from './Grid'
import Print from './Print'
import GoogleFonts from './GoogleFonts'
import Catch from './Catch'
import QueryString from './QueryString'
import Keyboard from './Keyboard'
import Storage from './Storage'
import QueryString from './QueryString'
import Style from './Style'
const NORMAL = 'normal'
const PRESENTER = 'presenter'
@ -30,156 +33,152 @@ const modes = {
const BaseWrapper = props => <>{props.children}</>
export class MDXDeck extends React.Component {
constructor(props) {
super(props)
const getIndex = ({ basepath }) => {
const { pathname } = globalHistory.location
const root = pathname.replace(basepath, '')
const n = Number(root.split('/')[1])
const index = isNaN(n) ? 0 : n
return index
}
this.state = {
slides: props.slides,
step: 0,
mode: NORMAL,
update: fn => this.setState(fn),
}
const mergeState = (state, next) =>
merge({}, state, typeof next === 'function' ? next(state) : next)
const useState = init => useReducer(mergeState, init)
const getWrapper = mode => {
switch (mode) {
case PRESENTER:
return Presenter
case OVERVIEW:
return Overview
case GRID:
return Grid
default:
return BaseWrapper
break
}
}
export const MDXDeckContext = React.createContext()
const useDeckState = () => {
const context = useContext(MDXDeckContext)
if (context) return context
const [state, setState] = useState({
metadata: {},
step: 0,
mode: NORMAL,
})
return {
state,
setState,
}
}
export const MDXDeckState = ({ children }) => {
const context = useDeckState()
return <MDXDeckContext.Provider value={context} children={children} />
}
export const MDXDeck = props => {
const { slides, basepath } = props
const { state, setState } = useDeckState(MDXDeckContext)
const index = getIndex(props)
const getMeta = i => {
return state.metadata[i] || {}
}
getIndex = () => {
const { basepath } = this.props
const { pathname } = globalHistory.location
const pagepath = pathname.replace(basepath, '')
const n = Number(pagepath.split('/')[1])
const index = isNaN(n) ? 0 : n
return index
const register = (index, meta) => {
setState({
metadata: {
...state.metadata,
[index]: {
...state.metadata[index],
...meta,
},
},
})
}
getMeta = i => {
const { slides } = this.state
const { meta = {} } = slides[i] || {}
return meta
}
goto = i => {
const { basepath } = this.props
const current = this.getIndex()
const reverse = i < current
navigate(basepath + '/' + i)
const meta = this.getMeta(i)
this.setState({
const goto = nextIndex => {
const current = getIndex(props)
const reverse = nextIndex < current
const { search } = globalHistory.location
navigate(basepath + '/' + nextIndex + search)
const meta = getMeta(nextIndex)
setState({
step: reverse ? meta.steps || 0 : 0,
})
}
previous = () => {
const { step } = this.state
const index = this.getIndex()
const meta = this.getMeta(index)
if (meta.steps && step > 0) {
this.setState(state => ({
step: state.step - 1,
}))
const previous = () => {
const current = getIndex(props)
const meta = getMeta(current)
if (meta.steps && state.step > 0) {
setState({ step: state.step - 1 })
} else {
const previous = index - 1
if (previous < 0) return
this.goto(previous)
const p = current - 1
if (p < 0) return
goto(p)
}
}
next = () => {
const { slides, step } = this.state
const index = this.getIndex()
const meta = this.getMeta(index)
if (meta.steps && step < meta.steps) {
this.setState(state => ({
step: state.step + 1,
}))
const next = () => {
const current = getIndex(props)
const meta = getMeta(current)
if (meta.steps && state.step < meta.steps) {
setState({ step: state.step + 1 })
} else {
const next = index + 1
if (next > slides.length - 1) return
this.goto(next)
const n = current + 1
if (n > slides.length - 1) return
goto(n)
}
}
register = (index, meta) => {
const { slides } = this.state
const initialMeta = slides[index].meta || {}
slides[index].meta = { ...initialMeta, ...meta }
this.setState({ slides })
const context = {
...state,
update: setState,
register,
modes,
index,
goto,
previous,
next,
}
componentDidCatch(err) {
console.error('componentDidCatch')
console.error(err)
}
const [First] = slides
const Wrapper = getWrapper(state.mode)
render() {
const { basepath } = this.props
const { pathname } = globalHistory.location
const { slides } = this.state
const pagepath = pathname.replace(basepath, '')
const mode = pagepath === '/print' ? PRINT : this.state.mode
const index = this.getIndex()
const context = {
...this.state,
register: this.register,
modes,
previous: this.previous,
next: this.next,
}
const [FirstSlide] = slides
let Wrapper = BaseWrapper
switch (mode) {
case PRESENTER:
Wrapper = Presenter
break
case OVERVIEW:
Wrapper = Overview
break
case GRID:
Wrapper = Grid
break
default:
break
}
const style =
mode !== modes.PRINT ? (
<Global
styles={{
body: {
overflow: 'hidden',
},
}}
/>
) : null
return (
<Provider {...this.props} {...this.state} mode={mode} index={index}>
{style}
<Catch>
<QueryString {...this.state} modes={modes} index={index} />
<Keyboard {...this.props} {...context} />
<Storage {...this.state} goto={this.goto} index={index} />
<GoogleFonts />
<Wrapper {...this.props} {...this.state} modes={modes} index={index}>
<Swipeable onSwipedRight={this.previous} onSwipedLeft={this.next}>
<Router basepath={basepath}>
<Slide path="/" index={0} context={context}>
<FirstSlide path="/" />
return (
<Provider {...props} {...context}>
<Catch>
<Style {...context} />
<Keyboard {...props} {...context} />
<Storage {...context} />
<QueryString {...context} />
<GoogleFonts />
<Wrapper {...props} {...context}>
<Swipeable onSwipedRight={previous} onSwipedLeft={next}>
<Router basepath={basepath}>
<Slide path="/" index={0} context={context}>
<First path="/" />
</Slide>
{slides.map((Component, i) => (
<Slide key={i} path={i + '/*'} index={i} context={context}>
<Component path={i + '/*'} />
</Slide>
{slides.map((Component, i) => (
<Slide key={i} path={i + '/*'} index={i} {...context}>
<Component path={i + '/*'} />
</Slide>
))}
<Print path="print" {...this.props} />
</Router>
</Swipeable>
</Wrapper>
</Catch>
</Provider>
)
}
))}
<Print path="print" {...props} />
</Router>
</Swipeable>
</Wrapper>
</Catch>
</Provider>
)
}
MDXDeck.propTypes = {

View File

@ -1,25 +1,17 @@
import React from 'react'
import { withContext } from './context'
import React, { useEffect } from 'react'
import { useDeck } from './context'
export const Notes = withContext(
class extends React.Component {
constructor(props) {
super(props)
const { context, children } = props
if (
!context ||
typeof context.index === 'undefined' ||
typeof context.register !== 'function'
) {
return
}
context.register(context.index, { notes: children })
}
export const Notes = props => {
const context = useDeck()
useEffect(() => {
if (!context || !context.register) return
if (typeof context.index === 'undefined') return
context.register(context.index, {
notes: props.children,
})
}, [])
render() {
return false
}
}
)
return false
}
export default Notes

View File

@ -1,13 +1,10 @@
import React, { useEffect } from 'react'
import { navigate } from '@reach/router'
import Zoom from './Zoom'
import Slide from './Slide'
import Pre from './Pre'
const query = '?mode=overview'
export const Overview = props => {
const { index, slides, basepath } = props
const { goto, index, slides } = props
const activeThumb = React.createRef()
useEffect(() => {
@ -44,7 +41,7 @@ export const Overview = props => {
key={i}
role="link"
onClick={e => {
navigate(basepath + '/' + i + query)
goto(i)
}}
style={{
display: 'block',

View File

@ -6,10 +6,10 @@ import Pre from './Pre'
import Clock from './Clock'
export const Presenter = props => {
const { slides, index } = props
const { slides, metadata, index } = props
const Current = slides[index]
const Next = slides[index + 1]
const { notes } = Current.meta || {}
const { notes } = metadata[index] || {}
return (
<div

View File

@ -25,7 +25,7 @@ export default ({ mode, modes, update, index }) => {
if (!search) return
navigate(pathname)
}
}, [index, mode])
}, [mode])
return false
}

View File

@ -72,7 +72,6 @@ export const Slide = ({ index, context, ...props }) => (
value={{
index,
...context,
...props,
}}
>
<Root {...props} />
@ -80,7 +79,9 @@ export const Slide = ({ index, context, ...props }) => (
)
Slide.defaultProps = {
step: Infinity,
context: {
step: Infinity,
},
}
export default Slide

View File

@ -1,21 +1,10 @@
import React from 'react'
import { withContext } from './context'
import { useDeck } from './context'
import useSteps from './useSteps'
export const Steps = withContext(
class extends React.Component {
constructor(props) {
super(props)
const { register, index } = props.context
const { length } = props
if (typeof register !== 'function') return
register(index, { steps: length })
}
render() {
const { context, render } = this.props
const { step } = context
return render({ step })
}
}
)
export const Steps = props => {
const step = useSteps(props.length)
return props.render({ step })
}
export default Steps

View File

@ -1,15 +1,27 @@
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
const STORAGE_INDEX = 'mdx-slide'
const STORAGE_STEP = 'mdx-step'
export const useLocalStorage = (handler, args = []) => {
const [focused, setFocused] = useState(false)
const handleFocus = () => {
setFocused(true)
}
const handleBlur = () => {
setFocused(false)
}
useEffect(() => {
window.addEventListener('storage', handler)
setFocused(document.hasFocus())
if (!focused) window.addEventListener('storage', handler)
window.addEventListener('focus', handleFocus)
window.addEventListener('blur', handleBlur)
return () => {
window.removeEventListener('storage', handler)
if (!focused) window.removeEventListener('storage', handler)
window.removeEventListener('focus', handleFocus)
window.removeEventListener('blur', handleBlur)
}
}, [...args])
}, [focused, ...args])
}
export const useSetStorage = (key, value) => {

View File

@ -0,0 +1,24 @@
import React from 'react'
import { globalHistory } from '@reach/router'
import { Global } from '@emotion/core'
const isPrintPath = () => {
const { pathname } = globalHistory.location
const parts = pathname.split('/')
const path = parts[parts.length - 1]
return path === 'print'
}
export default ({ mode, modes }) => {
if (mode === modes.PRINT) return false
if (isPrintPath()) return false
return (
<Global
styles={{
body: {
overflow: 'hidden',
},
}}
/>
)
}

View File

@ -0,0 +1,35 @@
import React, { useContext } from 'react'
import { render, cleanup } from 'react-testing-library'
import renderer from 'react-test-renderer'
import { MDXDeckState, MDXDeckContext, MDXDeck } from '../MDXDeck'
afterEach(cleanup)
const slides = [() => <pre>one</pre>, () => <pre>two</pre>]
describe('MDXDeckState', () => {
test('provides state', () => {
let context
const Consumer = props => {
context = useContext(MDXDeckContext)
return false
}
const deck = renderer.create(
<MDXDeckState>
<Consumer />
</MDXDeckState>
)
expect(typeof context.state).toBe('object')
expect(typeof context.state.metadata).toBe('object')
expect(context.state.step).toBe(0)
expect(context.state.mode).toBe('normal')
expect(typeof context.setState).toBe('function')
})
test.todo('setState updates state')
})
test('renders', () => {
const json = renderer.create(<MDXDeck slides={slides} />).toJSON()
expect(json).toMatchSnapshot()
})

View File

@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders 1`] = `
<div>
<div
role="group"
style={
Object {
"outline": "none",
}
}
tabIndex="-1"
>
<div
className="css-suf238-Root ekm8v3h0"
>
<div
role="group"
style={
Object {
"outline": "none",
}
}
tabIndex="-1"
>
<pre>
one
</pre>
</div>
</div>
</div>
</div>
`;

View File

@ -1,11 +1,11 @@
// slide context
import React, { useContext } from 'react'
export const Context = React.createContext({})
export const withContext = Component => props => (
<Context.Consumer
children={context => <Component {...props} context={context} />}
/>
)
export const useDeck = () => useContext(Context)
export const withContext = Component => props => {
const context = useDeck()
return <Component {...props} context={context} />
}

View File

@ -1,4 +1,4 @@
export { MDXDeck } from './MDXDeck'
export { MDXDeck, MDXDeckState } from './MDXDeck'
export { Head } from './Head'
export { Image } from './Image'
export { Notes } from './Notes'

View File

@ -1,10 +1,10 @@
import { useContext, useMemo } from 'react'
import { useContext, useEffect } from 'react'
import { Context } from './context'
export default length => {
const context = useContext(Context)
const { register, index, step } = context
useMemo(() => {
useEffect(() => {
if (typeof register !== 'function') return
register(index, { steps: length })
}, [length])

View File

@ -23,7 +23,8 @@ module.exports = async opts => {
switch (type) {
case 'pdf':
await page.goto(`http://localhost:${port}/print`, {
const url = `http://localhost:${port}/print`
await page.goto(url, {
waitUntil: 'networkidle2',
})
await page.pdf({

View File

@ -8,8 +8,8 @@
"mdx-deck-export": "./cli.js"
},
"scripts": {
"pdf": "./cli.js pdf ../../docs/demo.mdx",
"png": "./cli.js png ../../docs/demo.mdx"
"pdf": "./cli.js pdf ../../docs/demo.mdx -d ../../docs/dist",
"png": "./cli.js png ../../docs/demo.mdx -d ../../docs/dist"
},
"dependencies": {
"mdx-deck": "^2.2.3",

View File

@ -0,0 +1 @@
export { wrapPageElement } from './src'

View File

@ -1 +1,6 @@
export default {}
import React from 'react'
import { MDXDeckState } from '@mdx-deck/components'
export const wrapPageElement = ({ props, element }) => (
<MDXDeckState key={props.pageContext.basepath}>{element}</MDXDeckState>
)

View File

@ -1,22 +1,17 @@
import React from 'react'
import { MDXProvider } from '@mdx-js/react'
import { MDXDeck, splitSlides } from '@mdx-deck/components'
import Root from './root'
const wrapper = page => props => (
<MDXDeck
{...splitSlides({ ...props })}
basepath={page.pageContext.basepath}
/>
)
const wrapper = props => <MDXDeck {...splitSlides({ ...props })} />
export default props => {
const components = {
wrapper: wrapper(props),
}
const components = {
wrapper,
}
export default ({ Component, ...props }) => {
return (
<Root>
<MDXProvider components={components}>{props.children}</MDXProvider>
<Component {...props} components={components} />
</Root>
)
}

View File

@ -5,8 +5,9 @@ import Layout from '../layouts/deck'
export default props => {
const { mdx } = props.data
const children = <MDXRenderer children={mdx.code.body} />
return <Layout {...props} children={children} />
const Component = props => <MDXRenderer {...props} children={mdx.code.body} />
return <Layout Component={Component} basepath={props.pageContext.basepath} />
}
export const query = graphql`

View File

@ -747,7 +747,7 @@
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2":
version "7.4.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.3.tgz#79888e452034223ad9609187a0ad1fe0d2ad4bdc"
integrity sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==
@ -1823,6 +1823,11 @@
dependencies:
any-observable "^0.3.0"
"@sheerun/mutationobserver-shim@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==
"@stefanprobst/lokijs@^1.5.6-b":
version "1.5.6-b"
resolved "https://registry.yarnpkg.com/@stefanprobst/lokijs/-/lokijs-1.5.6-b.tgz#6a36a86dbe132e702e6b15ffd3ce4139aebfe942"
@ -5002,6 +5007,16 @@ dom-serializer@0, dom-serializer@~0.1.0:
domelementtype "^1.3.0"
entities "^1.1.1"
dom-testing-library@^3.19.0:
version "3.19.3"
resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.19.3.tgz#fba399987be1bdd57b07c4bc3ef46c3c084b26d9"
integrity sha512-oiI+oq91iO/Vpp+pt8PqfqLfBK074FH0eprhoFNvBCvJOk7vL4ozbe/yj/kEEGR6kiT4F3MAam19AX1fdGFjrA==
dependencies:
"@babel/runtime" "^7.3.4"
"@sheerun/mutationobserver-shim" "^0.3.2"
pretty-format "^24.5.0"
wait-for-expect "^1.1.0"
dom-walk@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
@ -11470,7 +11485,7 @@ pretty-error@^2.1.1:
renderkid "^2.0.1"
utila "~0.4"
pretty-format@^24.7.0:
pretty-format@^24.5.0, pretty-format@^24.7.0:
version "24.7.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.7.0.tgz#d23106bc2edcd776079c2daa5da02bcb12ed0c10"
integrity sha512-apen5cjf/U4dj7tHetpC7UEFCvtAgnNZnBDkfPv3fokzIqyOJckAG9OlAPC1BlFALnqT/lGB2tl9EJjlK6eCsA==
@ -11901,6 +11916,14 @@ react-test-renderer@^16.8.4:
react-is "^16.8.6"
scheduler "^0.13.6"
react-testing-library@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.1.2.tgz#f6bba6eeecedac736eb00b22b4c70bae04535a4f"
integrity sha512-z69lhRDGe7u/NOjDCeFRoe1cB5ckJ4656n0tj/Fdcr6OoBUu7q9DBw0ftR7v5i3GRpdSWelnvl+feZFOyXyxwg==
dependencies:
"@babel/runtime" "^7.4.2"
dom-testing-library "^3.19.0"
react@^16.8.3, react@^16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
@ -14432,6 +14455,11 @@ w3c-hr-time@^1.0.1:
dependencies:
browser-process-hrtime "^0.1.2"
wait-for-expect@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.1.tgz#9cd10e07d52810af9e0aaf509872e38f3c3d81ae"
integrity sha512-vd9JOqqEcBbCDhARWhW85ecjaEcfBLuXgVBqatfS3iw6oU4kzAcs+sCNjF+TC9YHPImCW7ypsuQc+htscIAQCw==
walker@^1.0.7, walker@~1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"