Skip to content

Commit

Permalink
chore(ui): migrate AppShellProvider and CodeBlock to TypeScript (#547)
Browse files Browse the repository at this point in the history
* chore(ui): migrate AppShellProvider to TypeScript

* fix(ui): fix index and wrapper

* fix(ui): fix index

* fix(ui): fix test

* fix(ui): use CodeBlock in AppShell stories

* fix(ui): fix types

* fix(ui): typings fix

* fix(ui): fix types

* fix(ui): hanful of improvements

* fix(ui): fix format

* fix(ui): few improvements

* fix(ui): timer type fix

* fix(ui): change default value

* fix(ui): theme typing fix

* fix(ui): inherited class fix

* fix(ui): add comment to improve storybook

---------

Co-authored-by: [email protected] <[email protected]>
Co-authored-by: Andreas Pfau <[email protected]>
Co-authored-by: hodanoori <[email protected]>
  • Loading branch information
4 people authored Nov 5, 2024
1 parent d2957ff commit a278544
Show file tree
Hide file tree
Showing 18 changed files with 165 additions and 66 deletions.
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
)
}

/**
* 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 {
/** 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", () => {
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")
})
}
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}
</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
/** 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", () => {
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

0 comments on commit a278544

Please sign in to comment.