Skip to content

Commit

Permalink
Add Stepper, CodePane highlighting/scrolling (#843)
Browse files Browse the repository at this point in the history
  • Loading branch information
treyhoover authored Feb 25, 2020
1 parent 1dd2adf commit aea2ec0
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 18 deletions.
16 changes: 16 additions & 0 deletions docs/content/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,15 @@ Appear is a component that makes a component animate on the slide on key press.

CodePane is a component for showing a syntax-highlighted block of source code. It will scroll for overflow amounts of code. The Code Pane will trim whitespace and normalize indents. It will also wrap long lines of code and preserve the indent. Optionally you can have the Code Pane fill the available empty space on your slide via the `autoFillHeight` prop. Themes are configurable objects and can be imported from the [prism-react-renderer themes](https://github.com/FormidableLabs/prism-react-renderer/tree/master/src/themes).

Additionally, `highlightStart` and `highlightEnd` props can be used to highlight certain ranges of code. Combine this with the [Stepper](#stepper) component to iterate over lines of code as you present.

| Props | Type | Example |
| -------------- | ----------------- | --------------------- |
| autoFillHeight | PropTypes.boolean | `false` |
| children | PropTypes.string | `let name = "Carlos"` |
| fontSize | PropTypes.number | `16` |
| highlightEnd | PropTypes.number | `2` |
| highlightStart | PropTypes.number | `1` |
| language | PropTypes.string | `javascript` |
| theme | Prism Theme ||

Expand All @@ -108,6 +112,18 @@ import lightTheme from 'prism-react-renderer/themes/nightOwlLight';
);
```

<a name="stepper"></a>

## Stepper

Stepper is a render-prop component that allows you to step over a set of values in your presentation, providing the current value and step as arguments in the child function. Like [Appear](#appear), this iteration happens on key press. Especially useful for stepping through the [Code Pane](#code-pane) component.

```jsx
<Stepper values={[1, 2, 3]}>
{(value, step) => <p>Current value: {value}</p>}
</Stepper>
```

<a name="full-screen"></a>

## FullScreen
Expand Down
53 changes: 48 additions & 5 deletions examples/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import {
Notes,
OrderedList,
Progress,
SpectacleLogo,
Slide,
SpectacleLogo,
Stepper,
Text,
indentNormalizer
indentNormalizer,
} from 'spectacle';

// SPECTACLE_CLI_THEME_START
Expand Down Expand Up @@ -129,9 +130,51 @@ const Presentation = () => (
</Slide>
<Slide transitionEffect="slide">
<Heading>Code Blocks</Heading>
<CodePane fontSize={18} language="cpp" autoFillHeight>
{cppCodeBlock}
</CodePane>
<Stepper
defaultValue={[]}
values={[
[1, 1],
[23, 25],
[40, 42]
]}
>
{(value, step) => (
<Box position="relative">
<CodePane
highlightStart={value[0]}
highlightEnd={value[1]}
fontSize={18}
language="cpp"
autoFillHeight
>
{cppCodeBlock}
</CodePane>

<Box
position="absolute"
bottom="0rem"
left="0rem"
right="0rem"
bg="black"
>
{/* This notes container won't appear for step 0 */}

{step === 1 && (
<Text fontSize="1.5rem" margin="0rem">
This is a note!
</Text>
)}

{step === 2 && (
<Text fontSize="1.5rem" margin="0rem">
You can use the stepper state to render whatever you like as
you step through the code.
</Text>
)}
</Box>
</Box>
)}
</Stepper>
<Text>
Code Blocks now auto size and scroll when there is an overflow of
content! They also auto-wrap longer lines.
Expand Down
16 changes: 12 additions & 4 deletions examples/one-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
Notes,
OrderedList,
Progress,
SpectacleLogo,
Slide,
SpectacleLogo,
Stepper,
Text,
indentNormalizer
} = Spectacle;
Expand Down Expand Up @@ -132,9 +133,16 @@
</${Slide}>
<${Slide} transitionEffect="slide">
<${Heading}>Code Blocks</${Heading}>
<${CodePane} fontSize=${18} language="cpp" autoFillHeight>
${cppCodeBlock}
</${CodePane}>
<${Stepper} defaultValue=${[]} values=${[[1, 1], [23, 25], [40, 42]]}>
${(value, step) => html`<${Box} position="relative">
<${CodePane} highlightStart=${value[0]} highlightEnd=${value[1]} fontSize=${18} language="cpp" autoFillHeight>
${cppCodeBlock}
</${CodePane}>
<${Box} position="absolute" bottom="0rem" left="0rem" right="0rem" bg="black">
${step === 1 && html`<${Text} fontSize="1.5rem" margin="0rem">This is a note!</${Text}>`}${step === 2 && html`<${Text} fontSize="1.5rem" margin="0rem">You can use the stepper state to render whatever you like as you step through the code.</${Text}>`}
</${Box}>
</${Box}>`}
</${Stepper}>
<${Text}>Code Blocks now auto size and scroll when there is an overflow of content! They also auto-wrap longer lines.</${Text}>
</${Slide}>
<${Slide}>
Expand Down
8 changes: 8 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ declare module 'spectacle' {
plain: Record<string, string>;
styles: Array<{ types: Array<string>; style: Record<string, string> }>;
};
highlightStart?: number;
highlightEnd?: number;
}>;

export const Stepper: React.FC<{
children: (value: any, step: number) => React.ReactNode;
values: any[];
defaultValue?: any;
}>;

type TypographyProps = {
Expand Down
33 changes: 30 additions & 3 deletions src/components/code-pane.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const lineNumberStyles = {
export default function CodePane(props) {
const canvas = React.useRef(document.createElement('canvas'));
const context = React.useRef(canvas.current.getContext('2d'));
const scrollContainerRef = React.useRef(null);
const lineRef = React.useRef(null);
const themeContext = React.useContext(ThemeContext);
const {
state: { printMode }
Expand Down Expand Up @@ -59,6 +61,12 @@ export default function CodePane(props) {
[font, fontSize, themeContext]
);

const isLineDimmed = React.useCallback(
lineNumber =>
lineNumber < props.highlightStart || lineNumber > props.highlightEnd,
[props.highlightStart, props.highlightEnd]
);

const measureIndentation = React.useCallback(
indentation => {
if (indentation === 0) {
Expand All @@ -72,6 +80,17 @@ export default function CodePane(props) {
[props.fontSize, font]
);

// Auto-scroll to highlighted range
React.useLayoutEffect(() => {
const lineHeight = lineRef.current.clientHeight;
const top = Math.max(0, (props.highlightStart - 1) * lineHeight);

scrollContainerRef.current.scroll({
top,
behavior: 'smooth'
});
}, [lineRef.current, props.highlightStart]);

return (
<>
<Highlight
Expand All @@ -82,25 +101,29 @@ export default function CodePane(props) {
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre
ref={scrollContainerRef}
className={`${className} ${props.autoFillHeight &&
'spectacle-auto-height-fill'}`}
style={{ ...style, ...preStyles }}
>
{tokens.map((line, i) => {
const lineProps = getLineProps({ line, key: i });
const lineIndentation = line[0].content.search(spaceSearch);

lineProps.style = {
...(lineProps.style || {}),
whiteSpace: 'pre-wrap',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start'
justifyContent: 'flex-start',
opacity: isLineDimmed(i + 1) ? 0.5 : 1
};
if (line[0].content && !line[0].empty) {
line[0].content = line[0].content.trimLeft();
}

return (
<div key={i} {...lineProps}>
<div key={i} {...lineProps} ref={i === 0 ? lineRef : undefined}>
<div style={lineNumberStyles}>{i + 1}</div>
<div
style={{
Expand Down Expand Up @@ -128,11 +151,15 @@ CodePane.propTypes = {
children: propTypes.string.isRequired,
fontSize: propTypes.number,
language: propTypes.string.isRequired,
highlightEnd: propTypes.number,
highlightStart: propTypes.number,
theme: propTypes.object
};

CodePane.defaultProps = {
language: 'javascript',
theme: theme,
fontSize: 15
fontSize: 15,
highlightStart: -Infinity,
highlightEnd: Infinity
};
14 changes: 9 additions & 5 deletions src/components/deck/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
DEFAULT_SLIDE_INDEX
} from '../../utils/constants';
import searchChildrenForAppear from '../../utils/search-children-appear';
import searchChildrenForStepper from '../../utils/search-children-stepper';
import OverviewDeck from './overview-deck';
import { Markdown, Slide, Notes } from '../../index';
import { isolateNotes, removeNotes } from '../../utils/notes';
Expand Down Expand Up @@ -138,11 +139,14 @@ const Deck = props => {
}

const slideElementMap = React.useMemo(() => {
const map = {};
filteredChildren.filter((slide, index) => {
map[index] = searchChildrenForAppear(slide.props.children);
});
return map;
return filteredChildren.reduce((map, slide, index) => {
const appearElements = searchChildrenForAppear(slide.props.children);
const stepperElements = searchChildrenForStepper(slide.props.children);

map[index] = appearElements + stepperElements;

return map;
}, {});
}, [filteredChildren]);

// Initialise useDeck hook and get state and dispatch off of it
Expand Down
20 changes: 20 additions & 0 deletions src/components/stepper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import propTypes from 'prop-types';
import { SlideContext } from '../hooks/use-slide';

const Stepper = ({ children: render, values, defaultValue }) => {
const {
state: { currentSlideElement: step }
} = React.useContext(SlideContext);

const value = step === -1 ? defaultValue : values[step];

return render(value, step);
};

Stepper.propTypes = {
values: propTypes.array.isRequired,
defaultValue: propTypes.array
};

export default Stepper;
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Deck from './components/deck';
import Slide from './components/slide';
import Appear from './components/appear';
import CodePane from './components/code-pane';
import Stepper from './components/stepper';
import {
OrderedList,
Quote,
Expand Down Expand Up @@ -53,6 +54,7 @@ export {
FullScreen,
Markdown,
SpectacleLogo,
Stepper,
Table,
TableCell,
TableRow,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/search-children-appear.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default function searchChildrenForAppear(children) {
return children.reduce((memo, current) => {
if (isComponentType(current, Appear.name)) {
memo += 1;
} else if (current.props.children && current.props.children.length > 0) {
} else if (current?.props?.children?.length > 0) {
memo += searchChildrenForAppear(current.props.children);
}
return memo;
Expand Down
18 changes: 18 additions & 0 deletions src/utils/search-children-stepper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import isComponentType from './is-component-type';
import Stepper from '../components/stepper';

export default function searchChildrenForStepper(children) {
if (!Array.isArray(children)) {
return 0;
}
return children.reduce((memo, current) => {
if (isComponentType(current, Stepper.name)) {
const { values } = current.props;

memo += Array.isArray(values) ? values.length : 0;
} else if (current?.props?.children?.length > 0) {
memo += searchChildrenForStepper(current.props.children);
}
return memo;
}, 0);
}
46 changes: 46 additions & 0 deletions src/utils/search-children-stepper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import Stepper from '../components/stepper';

import searchChildrenForStepper from './search-children-stepper';

Enzyme.configure({ adapter: new Adapter() });

describe('search children for stepper', () => {
it('returns 0 when there are no children', () => {
expect(searchChildrenForStepper([])).toEqual(0);
});

it('returns 0 when there are no stepper elements', () => {
expect(
searchChildrenForStepper([
<div key={0}>not a stepper element</div>,
<div key={1}>nor me</div>
])
).toEqual(0);
});

it('returns the amount of stepper elements', () => {
expect(
searchChildrenForStepper([
<div key={0}>not a stepper element</div>,
<Stepper key={1} values={[[1, 1]]}>
{() => 'But I have 1 value'}
</Stepper>,
<div key={2} />,
<Stepper
key={3}
values={[
[1, 1],
[2, 2],
[3, 3]
]}
>
{() => 'And I have 3 values'}
</Stepper>
])
).toEqual(4);
});
});

0 comments on commit aea2ec0

Please sign in to comment.