Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify code blocks into a single component #409

Merged
merged 47 commits into from
Jul 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
5de9cf8
chore: Install react-simple-code-editor
jerelmiller Jul 4, 2020
605c6f3
chore: Install babel-plugin-prismjs
jerelmiller Jul 4, 2020
867127a
chore: Add custom babelrc with prismjs babel plugin
jerelmiller Jul 4, 2020
cdef05b
chore: Use Prism from prismjs instead of prism-react-renderer
jerelmiller Jul 4, 2020
66cc42f
chore: Dont wrap pre in pre tag
jerelmiller Jul 4, 2020
b61401a
chore: Add code block component
jerelmiller Jul 4, 2020
e0b00d7
chore: Add css variables for nord theme
jerelmiller Jul 7, 2020
553af7a
chore: Add language as a class name for the code block
jerelmiller Jul 7, 2020
cede4ff
chore: Extract CodeBlock to CodeHighlight. Conditionally apply langua…
jerelmiller Jul 7, 2020
9650393
feat: Add a small button type
jerelmiller Jul 7, 2020
6390d3a
chore: Add copy button to CodeBlock
jerelmiller Jul 7, 2020
7bd13c6
chore: Add line numbers
jerelmiller Jul 7, 2020
eb9730c
chore: Add code formatting for code block
jerelmiller Jul 7, 2020
526718e
refactor: Extract CodeHighlight to own file
jerelmiller Jul 7, 2020
5f3dd27
feat: Add support for text wrapping in code highlight
jerelmiller Jul 7, 2020
ea895b4
chore: Add live editing support
jerelmiller Jul 7, 2020
c62c614
refactor: Extract some array helper methods
jerelmiller Jul 7, 2020
dddc708
feat: Add support for line highlighting
jerelmiller Jul 7, 2020
e62b60f
chore: Rename i to idx
jerelmiller Jul 7, 2020
32524b2
chore: Pass along highlighted lines from code block
jerelmiller Jul 7, 2020
a27260a
chore: Use CodeBlock instead of CodeSnippet in MDX
jerelmiller Jul 7, 2020
8745abf
chore: Remove CodeSnippet
jerelmiller Jul 7, 2020
c40dd76
chore: Default line numbers to false
jerelmiller Jul 7, 2020
0efe62e
chore: Fix eslint error
jerelmiller Jul 7, 2020
8ea9dd8
chore: Fix edit mode when line numbers are not displayed
jerelmiller Jul 7, 2020
9d32cc5
refactor: Lift state up to CodeBlock
jerelmiller Jul 7, 2020
15c97fc
chore: WIP live preview
jerelmiller Jul 7, 2020
ad7f59c
chore: Fix prop type error with button size
jerelmiller Jul 8, 2020
2f2a9b8
feat: add preview styles for code block
jerelmiller Jul 8, 2020
0f96781
chore: Use className from props instead of preview style
jerelmiller Jul 8, 2020
321acbf
feat: add a shallowEqual helper
jerelmiller Jul 8, 2020
0c6ca59
chore: Update useFormattedCode to useShallowMemo
jerelmiller Jul 8, 2020
fc688ad
feat: Pass format options in CodeBlock
jerelmiller Jul 8, 2020
551cb4b
chore: Check deps length when comparing in shallow equal
jerelmiller Jul 8, 2020
f1a8db7
chore: Ensure CodeBlock renders correctly in light mode
jerelmiller Jul 8, 2020
1bceb66
refactor: fully migrate to CodeBlock in ReferenceExample
jerelmiller Jul 8, 2020
45e319a
feat: update api and component reference templates to use the code bl…
jerelmiller Jul 8, 2020
fb464c6
chore: Disable previews in guides
jerelmiller Jul 8, 2020
8b47d85
docs: update component guide with changes to code blocks
jerelmiller Jul 8, 2020
2fb389d
refactor: Use CSS module name for line highlight instead of global name
jerelmiller Jul 8, 2020
ffed14c
fix: dont use window for component scope
jerelmiller Jul 8, 2020
c06efa2
chore: Add max height to code blocks
jerelmiller Jul 8, 2020
6872ffb
refactor: rename copy prop to copyable
jerelmiller Jul 8, 2020
0d192de
chore: Remove scope from mdx container
jerelmiller Jul 8, 2020
20712d3
feat: Add support for hcl syntax
jerelmiller Jul 8, 2020
c0cbc1c
chore: Add tests for the range and partition functions
jerelmiller Jul 8, 2020
a0c0ef3
refactor: Extract mdx code block into own component
jerelmiller Jul 8, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"presets": ["babel-preset-gatsby"],
"plugins": [
[
"babel-plugin-prismjs",
{
"languages": [
"css",
"hcl",
"javascript",
"json",
"jsx",
"ruby",
"shell",
"sql"
]
}
]
]
}
8 changes: 4 additions & 4 deletions COMPONENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,9 @@ A step description

> Note: keep in mind that a new line is necesary after an `img` tag to ensure proper rendering of subsequent text/markdown.

## Code Snippet
## Code blocks

Code Snippets are automatically formatted by three backticks. This is our preferred method to delineate code snippets, but it's worth noting that markdown will also consider any text that is indented 4 spaces (or 1 tab) to be a code block.
Code blocks are automatically formatted by three backticks. This is our preferred method to delineate code snippets, but it's worth noting that markdown will also consider any text that is indented 4 spaces (or 1 tab) to be a code block.

### Usage

Expand All @@ -193,10 +193,10 @@ There are four props that can be supplied to a code snippet.
```
````

- `lineNumbers`: `true` or `false`. Will show line numbers of the left side of the code, defaults to `true`.
- `lineNumbers`: `true` or `false`. Will show line numbers of the left side of the code, defaults to `false`.

````md
```jsx lineNumbers=false
```jsx lineNumbers=true
```
````

Expand Down
17 changes: 14 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@
"react-markdown": "^4.3.1",
"react-middle-ellipsis": "^1.1.0",
"react-shadow": "^18.1.2",
"react-simple-code-editor": "^0.11.0",
"use-dark-mode": "^2.3.1"
},
"devDependencies": {
"@newrelic/eslint-plugin-newrelic": "^0.3.0",
"@testing-library/react": "^10.0.4",
"babel-jest": "^26.0.1",
"babel-plugin-prismjs": "^2.0.1",
"babel-preset-gatsby": "^0.4.2",
"eslint": "^6.8.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
Expand Down
8 changes: 7 additions & 1 deletion src/components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ const Button = ({
children,
className,
variant,
size,
...props
}) => (
<Component
{...props}
className={cx(className, styles.button, styles[variant])}
className={cx(className, styles.button, styles[variant], styles[size])}
>
{children}
</Component>
Expand All @@ -24,10 +25,15 @@ Button.VARIANT = {
NORMAL: 'normal',
};

Button.SIZE = {
SMALL: 'small',
};

Button.propTypes = {
as: PropTypes.elementType,
children: PropTypes.node,
className: PropTypes.string,
size: PropTypes.oneOf(Object.values(Button.SIZE)),
type: PropTypes.oneOf(['button', 'submit', 'reset']),
variant: PropTypes.oneOf(Object.values(Button.VARIANT)).isRequired,
};
Expand Down
4 changes: 4 additions & 0 deletions src/components/Button.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@
color: var(--color-brand-400);
}
}

.small {
font-size: 0.75rem;
}
113 changes: 113 additions & 0 deletions src/components/CodeBlock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Button from './Button';
import CodeEditor from './CodeEditor';
import CodeHighlight from './CodeHighlight';
import FeatherIcon from './FeatherIcon';
import MiddleEllipsis from 'react-middle-ellipsis';
import { LiveError, LivePreview, LiveProvider } from 'react-live';
import styles from './CodeBlock.module.scss';
import useClipboard from '../hooks/useClipboard';
import useFormattedCode from '../hooks/useFormattedCode';

const defaultComponents = {
Preview: LivePreview,
};

const CodeBlock = ({
children,
components: componentOverrides = {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any instances where we are adding components that aren't the Preview? I'm a little unclear about the purpose of this prop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. <Preview /> is the only component right now that is customizable. To understand how I got here, let me describe a few of my goals for this refactor and some ways I considered designing this API.

Goals

  1. We can use this anywhere in the site that needs a code block.

Regardless of where it is rendered, it should be able to handle all use cases demanded of the code block. This includes:

  • conditionally showing line numbers
  • conditionally allowing people to copy the code
  • Allowing users to edit the code inline (useful for component docs)
  • Show a live preview of the code snippet (also useful for component docs)
  1. Allow this component to be used to show live previews in guides

While this is explicitly disabled currently, its something we can enable when we have some demand for this particular use case.

  1. With the upcoming move to a shared theme, we should be able to cut/paste this component into the shared theme library and use it across all sites.

The API is designed to be used everywhere. You'll notice that there is no developer website specific code in here. We want to be able to use this component in the open source site as well, which means we have to try and be as generic as possible.

API Design

Goal 3 presents an interesting challenge. Because we want to enable live previews on any code on any site, not just the NR1 SDK in the developer site, we can't use something like the code in <ReferencePreview /> which uses shadow DOM, loads the NR1 SDK's CSS, etc. On the flip side, because we want to be generic about this, this also presents a problem for our use case of needing style isolation for the NR1 SDK component previews. This means that the live preview MUST be customizable.

Specifically, the NR1 SDK component previews require the following:

  • Must be rendered in the shadow DOM to provide style isolation
  • Must be able to load an external stylesheet
  • Must be able to define inline CSS for additional styling (useful for guides like the <Grid /> which show boxes)
  • Must be able to render other components (used to mount the ToastManager when the Toast component is used)
  • Must be able to define additional styles on the root preview container (useful for examples like the <Spinner /> which renders absolutely positioned)

There are a couple of APIs I considered to allow this to be customizable with the previous requirements in mind:

  1. Allow extra props to be passed to configure the preview component:
<CodeBlock
  preview={true}
  previewExternalStylesheet="https://some.external.url/example.css"
  previewInShadowDOM={true}
  previewStyle={{ position: 'relative' }}
  previewChildren={() => <ToastManager />}
>
  import React from 'react'
  // the rest of the code that should be highlighted by the code block
</CodeBlock>

While these may not have been the final names of the props, you can see that the surface area of <CodeBlock /> now has grown a lot, specifically with a LOT of props dedicated to configuring the preview. This approach also means we need to add new props any time we have new uses cases for the preview that weren't already considered.

  1. Use the . delimited approach to give back control of the structure to the user of the component
import root from 'react-shadow'

<CodeBlock>
  <root.div>
    <link href='stylesheet.css' />
    <style type='text/css'>.h1 { font-weight: bold; }</style>
    <CodeBlock.Preview />
  </root.div>
  <CodeBlock.Code>
    import React from 'react'
    // the rest of the code that should be highlighted by the code block
  <CodeBlock.Code>
</CodeBlock>

While this approach is a bit better because I no longer have tons of preview props dedicated to configuring the component preview, I've now lost control of the structure of <CodeBlock /> and can't enforce it. For example, what if someone uses the component this way?

<CodeBlock>
  <CodeBlock.Code />
  <CodeBlock.Preview />
</CodeBlock>

Do we honor this structure and render the preview underneath the code? And what about the status bar that renders the file name and copy button? Are those lost? Should we provide a component for that to make it obvious it's being rendered?

As you can see, while I gain the ability to easily customize the structure of the preview, I've now lost an enforced structure for the entire thing and put the burden back into the hands of the user of the component to always make sure these are in the proper order. I didn't like this unnecessary burden since we want an enforced structure for the code block.

  1. Allow a user to provide their own component for the preview
const MyCustomPreview = ({ children, className }) => (
  <root.div>
    <link href="some.css" />
    {children}
  </root.div>
)

<CodeBlock
  preview={true}
  components={{ Preview: MyCustomPreview }}
/>

This is popular with libraries like React Select and even the MDXProvider for rendering mdx with customized components.

This approach allows us to be able to enforce the proper structure internally to the code block, but also provides the flexibility for the user to customize the structure of the preview component. This means users of the API can design the preview for any and all uses cases that may pop up over time without needing to touch the <CodeBlock /> to do so. This is the approach I opted for as I felt it provided the greatest flexibility without compromising the enforced structure we want in this component.

Final notes

Right now Preview is the only component that is allowed to be customized because it is the only one that needs it. I'm not sure it makes sense to provide a customized status bar, or customized code highlighting as of right now. If that use case ever exists, then we have a way to extend this component to allow for those customizations.

Hope that explains the reasoning behind this API design choice and why I chose to go the way I did.

copyable,
live,
highlightedLines,
fileName,
language,
lineNumbers,
preview,
scope,
formatOptions,
}) => {
const components = { ...defaultComponents, ...componentOverrides };
const formattedCode = useFormattedCode(children.trim(), formatOptions);
const [copied, copy] = useClipboard();
const [code, setCode] = useState(formattedCode);

useEffect(() => {
setCode(formattedCode);
}, [formattedCode]);

return (
<LiveProvider code={code} scope={scope}>
{preview && <components.Preview className={styles.preview} />}
<div className={cx(styles.container, { [styles.withPreview]: preview })}>
<div className={styles.scrollContainer}>
{live ? (
<CodeEditor
value={code}
language={language}
lineNumbers={lineNumbers}
onChange={setCode}
/>
) : (
<CodeHighlight
highlightedLines={highlightedLines}
language={language}
lineNumbers={lineNumbers}
>
{code}
</CodeHighlight>
)}
</div>

{(copyable || fileName) && (
<div className={styles.statusBar}>
<div className={styles.fileName}>
{fileName && (
<MiddleEllipsis>
<span title={fileName}>{fileName}</span>
</MiddleEllipsis>
)}
</div>
<Button
type="button"
className={styles.copyButton}
variant={Button.VARIANT.PLAIN}
onClick={() => copy(code)}
size={Button.SIZE.SMALL}
>
<FeatherIcon name="copy" className={styles.copyButtonIcon} />
{copied ? 'Copied' : 'Copy output'}
</Button>
</div>
)}
</div>
{(live || preview) && <LiveError className={styles.liveError} />}
</LiveProvider>
);
};

CodeBlock.propTypes = {
fileName: PropTypes.string,
components: PropTypes.shape({
Preview: PropTypes.elementType,
}),
copyable: PropTypes.bool,
children: PropTypes.string.isRequired,
formatOptions: PropTypes.object,
highlightedLines: PropTypes.string,
language: PropTypes.string,
lineNumbers: PropTypes.bool,
live: PropTypes.bool,
preview: PropTypes.bool,
scope: PropTypes.object,
};

CodeBlock.defaultProps = {
copyable: true,
lineNumbers: false,
live: false,
preview: false,
};

export default CodeBlock;
69 changes: 69 additions & 0 deletions src/components/CodeBlock.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
.container {
background: var(--color-nord-0);
border-radius: 4px;

:global(.light-mode) & {
background: var(--color-nord-6);
}

&.withPreview {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}

.scrollContainer {
max-height: 26em;
overflow: auto;
}

.statusBar {
color: var(--color-nord-6);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-nord-1);
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: 0 1rem;
font-size: 0.75rem;

:global(.light-mode) & {
color: var(--color-nord-0);
background: var(--color-nord-4);
}
}

.fileName {
font-family: var(--code-font);
white-space: nowrap;
overflow: hidden;
padding-right: 0.5rem;
}

.copyButton {
white-space: nowrap;
}

.copyButtonIcon {
margin-right: 0.5rem;
}

.liveError {
color: white;
background: var(--color-red-400);
padding: 0.5rem 1rem;
font-size: 0.75rem;
overflow: auto;
margin-top: 0.5rem;
border-radius: 2px;
}

.preview {
padding: 2rem;
background: var(--color-white);
border: 1px solid var(--color-neutrals-100);
box-shadow: var(--boxshadow);
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
43 changes: 43 additions & 0 deletions src/components/CodeEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Editor from 'react-simple-code-editor';
import CodeHighlight from './CodeHighlight';
import styles from './CodeEditor.module.scss';

const CodeEditor = ({ value, language, lineNumbers, onChange }) => {
const lineNumberWidth = value.trim().split('\n').length.toString().length;

return (
<Editor
value={value}
padding={16}
onValueChange={onChange}
highlight={(code) => (
<CodeHighlight
wrap
className={styles.editor}
language={language}
lineNumbers={lineNumbers}
>
{code}
</CodeHighlight>
)}
textareaClassName={cx({ [styles.lineNumbers]: lineNumbers })}
style={{
fontFamily: 'var(--code-font)',
fontSize: '0.75rem',
'--line-number-width': `${lineNumberWidth}ch`,
}}
/>
);
};

CodeEditor.propTypes = {
language: PropTypes.string.isRequired,
lineNumbers: PropTypes.bool,
onChange: PropTypes.func,
value: PropTypes.string.isRequired,
};

export default CodeEditor;
7 changes: 7 additions & 0 deletions src/components/CodeEditor.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.editor {
padding: 0 !important;
}

.lineNumbers {
padding-left: calc(2rem + var(--line-number-width)) !important;
}
Loading