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

chore(ui): migrate AppShellProvider and CodeBlock to TypeScript #547

Merged
merged 24 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .changeset/silent-maps-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudoperators/juno-ui-components": minor
---

Migrate AppShellProvider and CodeBlock to TypeScript
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from "react"

import { StyleProvider } from "../StyleProvider"
import { ShadowRoot } from "../ShadowRoot"
import { PortalProvider } from "../PortalProvider"
import { DEFAULT_THEME_NAME } from "../StyleProvider/StyleProvider.component"

const Wrapper = ({ children, shadowRoot, shadowRootMode }: WrapperProps) => {
return shadowRoot ? (
<ShadowRoot mode={shadowRootMode}>
<>{children}</>
</ShadowRoot>
) : (
children
gjaskiewicz marked this conversation as resolved.
Show resolved Hide resolved
)
}

/**
* This provider acts as a wrapper for Juno apps. It renders a StyleProvider and PortalProvider
*/
export const AppShellProvider = ({
shadowRoot = true,
shadowRootMode = "open",
stylesWrapper = "inline",
theme = DEFAULT_THEME_NAME,
children,
}: AppShellProviderProps) => {
return (
<Wrapper shadowRoot={shadowRoot} shadowRootMode={shadowRootMode}>
<StyleProvider theme={theme} stylesWrapper={shadowRoot ? "inline" : stylesWrapper}>
<PortalProvider>{children}</PortalProvider>
</StyleProvider>
</Wrapper>
)
}

export type AppShellStyleWrapper = "head" | "inline"

interface WrapperProps {
gjaskiewicz marked this conversation as resolved.
Show resolved Hide resolved
/** React nodes or a collection of React nodes to be rendered as content. */
children?: React.ReactNode
/** Whether the app is rendered inside a ShadowRoot. Only choose false if the app is meant to run as a stand-alone application. */
shadowRoot?: boolean
/** Shadow root mode */
shadowRootMode?: ShadowRootMode
}

export interface AppShellProviderProps extends WrapperProps {
/** Where app stylesheets are imported. This is only relevant if shadowRoot === false. If you use a ShadowRoot the styles must be inline. */
stylesWrapper?: AppShellStyleWrapper
/** theme: theme-dark or theme-light */
theme?: "theme-dark" | "theme-light"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
*/

import React from "react"
import { AppShellProvider } from "."
import { CodeBlock } from "../CodeBlock/index.js"
import { Message } from "../Message/Message.component"
import { Message } from "../Message"
import { AppShellProvider, AppShellProviderProps } from "./AppShellProvider.component"
import { CodeBlock } from "../CodeBlock"

export default {
title: "Layout/AppShellProvider",
Expand All @@ -18,7 +18,7 @@ export default {
},
}

const Template = (args) => <AppShellProvider {...args}>{args.children}</AppShellProvider>
const Template = (args: AppShellProviderProps) => <AppShellProvider {...args}>{args.children}</AppShellProvider>

export const Default = {
render: Template,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import * as React from "react"
import { render } from "@testing-library/react"
import { AppShellProvider } from "./AppShellProvider.component"

describe("AppShellProvider", () => {
gjaskiewicz marked this conversation as resolved.
Show resolved Hide resolved
test("renders an AppShellProvider wrapper div with 'theme-dark' theme class by default", () => {
render(<AppShellProvider shadowRoot={false} />)
expect(document.querySelector(".juno-app-body")).toHaveClass("theme-dark")
})

test("renders an AppShellProvider wrapper div with theme as passed", () => {
render(<AppShellProvider shadowRoot={false} theme="theme-light" />)
expect(document.querySelector("div.juno-app-body")).toHaveClass("theme-light")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* SPDX-License-Identifier: Apache-2.0
*/

export { AppShellProvider } from "./AppShellProvider.component"
export { AppShellProvider } from "../../deprecated_js/AppShellProvider/AppShellProvider.component"
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,23 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState, useRef } from "react"
import PropTypes from "prop-types"
import { JsonViewer } from "../../deprecated_js/JsonViewer/JsonViewer.component"
import { Icon } from "../../deprecated_js/Icon/index"
import React, { useState, useRef, useCallback } from "react"
import { JsonViewer } from "../JsonViewer"
import { Icon } from "../Icon"

const wrapperStyles = `
jn-bg-theme-code-block
jn-rounded
`

const preStyles = (wrap) => {
const preStyles = (wrap: boolean) => {
return `
jn-p-6
${wrap ? "jn-break-words jn-break-all jn-whitespace-pre-wrap" : "jn-overflow-x-auto"}
`
}

const sizeStyles = (size) => {
const sizeStyles = (size: CodeBlockSize) => {
switch (size) {
case "small":
return `
Expand Down Expand Up @@ -117,23 +116,33 @@ export const CodeBlock = ({
lang = "",
className = "",
...props
}) => {
}: CodeBlockProps) => {
const [isCopied, setIsCopied] = useState(false)
const timeoutRef = React.useRef(null)
const timeoutRef = React.useRef<number | null>(null)

React.useEffect(() => {
return () => clearTimeout(timeoutRef.current) // clear when component is unmounted
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
} // clear when component is unmounted
}, [])

const theCode = useRef(null)
const theCode = useRef<HTMLElement>(null)

const handleCopyClick = () => {
const textToCopy = lang === "json" ? JSON.stringify(content || children) : theCode.current.textContent
navigator.clipboard.writeText(textToCopy)
const handleCopyClick = useCallback(() => {
const textToCopy = lang === "json" ? JSON.stringify(content || children) : theCode.current?.textContent
if (textToCopy) {
navigator.clipboard.writeText(textToCopy).catch(() => {
console.warn("Cannot copy text to clipboard")
guoda-puidokaite marked this conversation as resolved.
Show resolved Hide resolved
})
}
setIsCopied(true)
clearTimeout(timeoutRef.current) // clear any possibly existing Refs
timeoutRef.current = setTimeout(() => setIsCopied(false), 1000)
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current) // clear any possibly existing Refs
}
timeoutRef.current = window.setTimeout(() => setIsCopied(false), 1000)
}, [content, children, lang])

return (
<div
Expand All @@ -153,7 +162,7 @@ export const CodeBlock = ({
) : (
<pre className={`juno-code-block-pre ${preStyles(wrap)} ${sizeStyles(size)}`}>
<code className={`${codeStyles}`} ref={theCode}>
{content || children}
{(content || children) as React.ReactNode}
gjaskiewicz marked this conversation as resolved.
Show resolved Hide resolved
</code>
</pre>
)}
Expand All @@ -170,21 +179,23 @@ export const CodeBlock = ({
)
}

CodeBlock.propTypes = {
type CodeBlockSize = "auto" | "small" | "medium" | "large"

export interface CodeBlockProps {
/** The content to render. Will override children if passed. */
content: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
content?: string | object
/** The children to render. Will be overridden by content prop if passed as well. */
children: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
children?: React.ReactNode | object
gjaskiewicz marked this conversation as resolved.
Show resolved Hide resolved
gjaskiewicz marked this conversation as resolved.
Show resolved Hide resolved
/** Pass at title to render. Will look like a single tab. */
heading: PropTypes.string,
heading?: string
/** Set whether the code should wrap or not. Default is true. */
wrap: PropTypes.bool,
wrap?: boolean
/** Set the size of the CodeBlock. Default is "auto" */
size: PropTypes.oneOf(["auto", "small", "medium", "large"]),
size?: CodeBlockSize
/** Render a button to copy the code to the clipboard. Defaults to true */
copy: PropTypes.bool,
copy?: boolean
/** Pass a lang prop. Passing "json" will render a fully-featured JsonView. Will also add a data-lang-attribute to the codeblock */
lang: PropTypes.string,
lang?: string
/** Add a custom className to the wrapper of the CodeBlock */
className: PropTypes.string,
className?: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
*/

import React from "react"
import PropTypes from "prop-types"
import { CodeBlock } from "./index.js"
import { Tabs } from "../../deprecated_js/Tabs/index.js"
import { TabList } from "../../deprecated_js/TabList/index.js"
import { Tab } from "../../deprecated_js/Tab/index.js"
import { TabPanel } from "../../deprecated_js/TabPanel/index.js"
import { CodeBlock } from "."
import { Tabs } from "../Tabs"
import { TabList } from "../TabList"
import { Tab } from "../Tab"
import { TabPanel } from "../TabPanel"

const TabStory = {
args: {
Expand All @@ -32,7 +31,7 @@ export default {
},
}

const TabsTemplate = ({ tabs, codeBlocks }) => (
const TabsTemplate = ({ tabs, codeBlocks }: TabsTemplateProps) => (
<Tabs variant="codeblocks">
<TabList>
{tabs.map((tab, t) => (
Expand All @@ -47,9 +46,9 @@ const TabsTemplate = ({ tabs, codeBlocks }) => (
</Tabs>
)

TabsTemplate.propTypes = {
tabs: PropTypes.array,
codeBlocks: PropTypes.array,
interface TabsTemplateProps {
tabs: (typeof Tab)[]
codeBlocks: (typeof CodeBlock)[]
}

export const Default = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@

import * as React from "react"
import { render, screen } from "@testing-library/react"
import { CodeBlock } from "./index"
import { CodeBlock } from "."

describe("CodeBlock", () => {
gjaskiewicz marked this conversation as resolved.
Show resolved Hide resolved
test("renders a CodeBlock with content as passed", async () => {
test("renders a CodeBlock with content as passed", () => {
render(<CodeBlock data-testid="codeblock" content="some example code" />)
expect(screen.getByTestId("codeblock")).toBeInTheDocument()
expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block")
expect(screen.getByTestId("codeblock")).toHaveTextContent("some example code")
})

test("renders a CodeBlock with children as passed", async () => {
test("renders a CodeBlock with children as passed", () => {
render(<CodeBlock data-testid="codeblock">{"some children here"}</CodeBlock>)
expect(screen.getByTestId("codeblock")).toBeInTheDocument()
expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block")
expect(screen.getByTestId("codeblock")).toHaveTextContent("some children here")
})

test("renders a CodeBlock with a lang attribute as passed", async () => {
test("renders a CodeBlock with a lang attribute as passed", () => {
render(<CodeBlock data-testid="codeblock" lang="javascript" />)
expect(screen.getByTestId("codeblock")).toBeInTheDocument()
expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block")
expect(screen.getByTestId("codeblock")).toHaveAttribute("data-lang", "javascript")
})

test("renders a wrapping CodeBlock by default", async () => {
test("renders a wrapping CodeBlock by default", () => {
render(<CodeBlock data-testid="codeblock" />)
expect(screen.getByTestId("codeblock")).toBeInTheDocument()
expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block")
Expand All @@ -39,7 +39,7 @@ describe("CodeBlock", () => {
expect(document.querySelector("pre")).not.toHaveClass("jn-overflow-x-auto")
})

test("renders a non-wrapping CodeBlock as passed", async () => {
test("renders a non-wrapping CodeBlock as passed", () => {
render(<CodeBlock data-testid="codeblock" wrap={false} />)
expect(screen.getByTestId("codeblock")).toBeInTheDocument()
expect(screen.getByTestId("codeblock")).toHaveClass("juno-code-block")
Expand All @@ -49,38 +49,38 @@ describe("CodeBlock", () => {
expect(document.querySelector("pre")).toHaveClass("jn-overflow-x-auto")
})

test("renders a CodeBlock without height restrictions by default", async () => {
test("renders a CodeBlock without height restrictions by default", () => {
render(<CodeBlock content="123" />)
expect(document.querySelector("pre")).toBeInTheDocument()
expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-small")
expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-medium")
expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-large")
})

test("renders a small sized CodeBlock as passed", async () => {
test("renders a small sized CodeBlock as passed", () => {
render(<CodeBlock content="123" size="small" />)
expect(document.querySelector("pre")).toBeInTheDocument()
expect(document.querySelector("pre")).toHaveClass("juno-codeblock-pre-small")
expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-medium")
expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-large")
})

test("renders a medium sized CodeBlock as passed", async () => {
test("renders a medium sized CodeBlock as passed", () => {
render(<CodeBlock content="123" size="medium" />)
expect(document.querySelector("pre")).toBeInTheDocument()
expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-small")
expect(document.querySelector("pre")).toHaveClass("juno-codeblock-pre-medium")
expect(document.querySelector("pre")).not.toHaveClass("juno-codeblock-pre-large")
})

test("renders a heading as passed", async () => {
test("renders a heading as passed", () => {
render(<CodeBlock data-testid="codeblock" content="123" heading="Look, a CodeBlock!" />)
expect(screen.getByTestId("codeblock")).toBeInTheDocument()
expect(document.querySelector(".juno-codeblock-heading")).toBeInTheDocument()
expect(document.querySelector(".juno-codeblock-heading")).toHaveTextContent("Look, a CodeBlock!")
})

test("renders a JSONView as passed", async () => {
test("renders a JSONView as passed", () => {
const testJson = {
someKey: "some value",
someOtherKey: 12,
Expand All @@ -92,7 +92,7 @@ describe("CodeBlock", () => {
expect(document.querySelector("[data-json-viewer]")).toBeInTheDocument()
})

test("renders a JSONView as passed with children", async () => {
test("renders a JSONView as passed with children", () => {
const testObj = {
someKey: "some value",
someOtherKey: 12,
Expand All @@ -108,18 +108,18 @@ describe("CodeBlock", () => {
expect(document.querySelector("[data-json-viewer]")).toBeInTheDocument()
})

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

test("renders a CodeBlock with className as passed", async () => {
test("renders a CodeBlock with className as passed", () => {
render(<CodeBlock data-testid="codeblock" className="my-class" />)
expect(screen.getByTestId("codeblock")).toBeInTheDocument()
expect(screen.getByTestId("codeblock")).toHaveClass("my-class")
})

test("renders a CodeBlock with all props as passed", async () => {
test("renders a CodeBlock with all props as passed", () => {
render(<CodeBlock data-testid="codeblock" data-lolol="code-lang-js" />)
expect(screen.getByTestId("codeblock")).toBeInTheDocument()
expect(screen.getByTestId("codeblock")).toHaveAttribute("data-lolol", "code-lang-js")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* SPDX-License-Identifier: Apache-2.0
*/

export { CodeBlock } from "./CodeBlock.component.js"
export { CodeBlock } from "./CodeBlock.component"
Loading
Loading