Skip to content

Commit

Permalink
[ui] Codeblock tabs (#185)
Browse files Browse the repository at this point in the history
* [ui] CodeBlock: add tabbed codeblocks

* [ui] CodeBlock: add tests for tabbed codeblock

* [ui] Codeblock: remove obsolete, adjust styles

* [ui] CodeBlock: add disabled test for copy to clipboard

The test currently fails due to a bug in react testing libarary (testing-library/user-event#839). Once this is resolved the test can be activated and should work
  • Loading branch information
franzheidl committed Jul 28, 2022
1 parent b953670 commit dd00cbd
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 39 deletions.
@@ -1,6 +1,10 @@
import React, { useState, useEffect } from "react"
import React, { useState, useEffect, useRef } from "react"
import PropTypes from "prop-types"
import { Icon } from "../Icon/Icon.component.js"
import { Tabs } from "../Tabs/Tabs.component.js"
import { TabList } from "../TabList/TabList.component.js"
import { Tab } from "../Tab/Tab.component.js"
import { TabPanel } from "../TabPanel/TabPanel.component.js"

const codeBlockBaseStyles = `
jn-bg-theme-code-block
Expand Down Expand Up @@ -39,29 +43,16 @@ const codeContainerSize = (size) => {

}



const codeStyles = `
jn-text-sm
`

const titleBarStyles = `
jn-border-b-[1px]
jn-border-theme-codeblock-bar
`

const tabStyles = `
jn-font-bold
jn-text-sm
jn-inline-block
jn-px-6
jn-py-2
`

const tabStylesActive = `
jn-border-b-[3px]
`

const bottomBarStyles = `
jn-flex
jn-justify-end
Expand All @@ -75,6 +66,8 @@ const bottomBarStyles = `
export const CodeBlock = ({
wrap,
heading,
tabs,
contents,
size,
copyToClipboard,
className,
Expand All @@ -91,36 +84,58 @@ export const CodeBlock = ({
}, [])

const handleCopyClick = () => {
navigator.clipboard.writeText(children)
navigator.clipboard.writeText(theCode.current.textContent)
setIsCopied(true)
clearTimeout(timeoutRef.current) // clear any possibly existing Refs
timeoutRef.current = setTimeout(() => setIsCopied(false), 1000)
}

const codeBlockHeading = (
<div className={`juno-codeblock-titlebar ${titleBarStyles}`}>
<span className={`juno-codeblock-tab ${tabStyles} ${tabStylesActive}`}>
{heading}
</span>
</div>
)
/* If tabs were passed, use these. If not, but heading was been passed, create an array with heading as only element. Otherwise, return empty array: */
const theTabs = tabs.length ? tabs : (heading.length ? [heading] : [])

/* If contents was passed, use these. If not, but heading was passed, create an array with children as the only element. Otherwise, return empty array: */
const theContents = contents.length ? contents : (children ? [children] : [])

const copy = (
<div className={`juno-codeblock-bottombar ${bottomBarStyles}`}>
<span className={`jn-font-bold jn-text-sm jn-mr-4 jn-mt-1`} >{ isCopied ? "Copied!" : "" }</span>
<Icon icon="contentCopy" onClick={handleCopyClick} />
</div>
)
const theCode = useRef(null)

return (

<div className={`juno-codeblock ${codeBlockBaseStyles} ${className}`} {...props} >
{ heading ? codeBlockHeading : null }
<pre className={`${codeContainerStyles(wrap)} ${codeContainerSize(size)}`} data-testid="juno-codeblock-pre">
<code className={`${codeStyles}`} >
{children}
</code>
</pre>
{ copyToClipboard ? copy : null }

{ theTabs.length ?
<Tabs>
<TabList>
{theTabs.map((tab, t) => (
<Tab className={`${tabStyles}`} key={t}>{tab}</Tab>
))}
</TabList>
{theContents.map((element, c) => (
<TabPanel key={c}>
<pre className={`${codeContainerStyles(wrap)} ${codeContainerSize(size)}`} data-testid="juno-codeblock-pre">
<code className={`${codeStyles}`} ref={theCode} >
{element}
</code>
</pre>
</TabPanel>
))}
</Tabs>
:
<pre className={`${codeContainerStyles(wrap)} ${codeContainerSize(size)}`} data-testid="juno-codeblock-pre">
<code className={`${codeStyles}`} ref={theCode} >
{children}
</code>
</pre>
}

{ copyToClipboard ?
<div className={`juno-codeblock-bottombar ${bottomBarStyles}`}>
<span className={`jn-font-bold jn-text-sm jn-mr-4 jn-mt-1`} >{ isCopied ? "Copied!" : "" }</span>
<Icon icon="contentCopy" onClick={handleCopyClick} />
</div>
:
null
}

</div>
)
}
Expand All @@ -134,6 +149,10 @@ CodeBlock.propTypes = {
wrap: PropTypes.bool,
/** Optional title */
heading: PropTypes.string,
/** Optional tabs. Pass an array of strings to be rendered as tabs */
tabs: PropTypes.arrayOf(PropTypes.string),
/* Optional contents. Pass an array of code sample strings to be rendered to work with tabs: */
contents: PropTypes.arrayOf(PropTypes.string),
/** Optional size (height). By default height is unrestricted. If specifying a size the CodeBlock will not grow past the given size and get scrollbars if the content is higher */
size: PropTypes.oneOf(["auto", "small", "medium", "large"]),
/** Whether to display a 'Copy to Clipboard' button */
Expand All @@ -143,6 +162,8 @@ CodeBlock.propTypes = {
CodeBlock.defaultProps = {
wrap: true,
heading: "",
tabs: [],
contents: [],
size: "auto",
copyToClipboard: true,
className: "",
Expand Down
Expand Up @@ -19,20 +19,38 @@ Default.parameters = {
},
}
Default.args = {
heading: "My code",
children: "Some code goes here",
}

export const DefaultNoHeading = Template.bind({})
DefaultNoHeading.parameters = {
export const WithHeading = Template.bind({})
WithHeading.parameters = {
docs: {
description: {
story: "Default CodeBlock, no heading",
},
},
}
DefaultNoHeading.args = {
WithHeading.args = {
children: "Some code goes here",
heading: "My code",
}

export const WithTabs = Template.bind({})
WithTabs.parameters = {
docs: {
description: {
story: "Tabbed Codeblock"
}
}
}
WithTabs.args = {
tabs: ["React.js", "Vue.js", "Svelte.js", "html"],
contents: [
"<ReactComponent />",
"<VueComponent />",
"<SvelteComponent />",
"<div>Component</div>",
]
}

export const MultiLineWithHeading = Template.bind({})
Expand Down
31 changes: 31 additions & 0 deletions libs/juno-ui-components/src/components/CodeBlock/CodeBlock.test.js
@@ -1,5 +1,6 @@
import * as React from "react"
import { render, screen} from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { CodeBlock } from "./index"

describe("CodeBlock", () => {
Expand Down Expand Up @@ -46,6 +47,36 @@ describe("CodeBlock", () => {
expect(screen.getByTestId("juno-codeblock-pre")).toHaveClass("jn-overflow-y-auto")
})

test("renders a CodeBlock with a Copy button by default", async () => {
render(<CodeBlock />)
expect(screen.getByRole("button", {name: "contentCopy"})).toBeInTheDocument()
})

test("renders a tabbed codeblock as passed", async () => {
const tabs = ["tab-a", "tab-b", "tab-c"]
const contents = ["a-content", "b-content", "c-content"]
render(<CodeBlock contents={contents} tabs={tabs} />)
expect(screen.getByRole("tab", {name: "tab-a"})).toBeInTheDocument()
expect(screen.getByRole("tab", {name: "tab-b"})).toBeInTheDocument()
expect(screen.getByRole("tab", {name: "tab-c"})).toBeInTheDocument()
expect(screen.getByRole("tab", {name: "tab-a"})).toHaveAttribute("aria-selected", "true")
expect(screen.getByText("a-content")).toBeInTheDocument()
userEvent.click(screen.getByRole("tab", { name: 'tab-c' }))
expect(screen.getByRole("tab", {name: "tab-c"})).toHaveAttribute("aria-selected", "true")
expect(screen.getByRole("tab", {name: "tab-a"})).not.toHaveAttribute("aria-selected", "true")
expect(screen.getByText("c-content")).toBeInTheDocument()
})


/* Uncomment test below once https://github.com/testing-library/user-event/issues/839 is resolved: */
// test("copies Codeblock content to the clipboard", async () => {
// const user = userEvent.setup()
// render(<CodeBlock>yadayada</CodeBlock>)
// await user.click(screen.getByRole("button", {name: "contentCopy"}))
// const clipboardText = await navigator.clipboard.readText();
// expect(clipboardText).toBe("yadayada");
// })

test("renders all props as passed", async () => {
render(<CodeBlock data-testid="my-codeblock" data-lolol="some-props"/>)
expect(screen.getByTestId("my-codeblock")).toBeInTheDocument()
Expand Down

0 comments on commit dd00cbd

Please sign in to comment.