Skip to content

Commit

Permalink
feat: convert packages to TypeScript (#568)
Browse files Browse the repository at this point in the history
* feat: convert packages to TypeScript

* fix: use extension-less import

* feat: mark additional properties as optional

* feat: add TypeScript types to CodeBlock

* feat: add TypeScript types to CodeTabsContext

* fix: prevent infinite update loop

* fix: use correct default Context value

* fix: mark link type as optional
  • Loading branch information
dstaley committed Apr 25, 2022
1 parent 0f3d31f commit 26918b9
Show file tree
Hide file tree
Showing 27 changed files with 297 additions and 55 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-lions-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-command-line-terminal': major
---

Add TypeScript types
5 changes: 5 additions & 0 deletions .changeset/perfect-swans-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-docs-page': major
---

Add TypeScript types
5 changes: 5 additions & 0 deletions .changeset/rich-bears-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-enterprise-alert': major
---

Add TypeScript types
5 changes: 5 additions & 0 deletions .changeset/shiny-jars-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-code-block': major
---

Add TypeScript types
5 changes: 5 additions & 0 deletions .changeset/stupid-items-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-hashi-stack-menu': major
---

Add TypeScript types
5 changes: 5 additions & 0 deletions .changeset/stupid-laws-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-text-split': major
---

Add TypeScript types
5 changes: 5 additions & 0 deletions .changeset/three-schools-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-sentinel-embedded': major
---

Add TypeScript types
5 changes: 5 additions & 0 deletions .changeset/wet-papayas-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-call-to-action': major
---

Add TypeScript types
5 changes: 5 additions & 0 deletions .changeset/wild-panthers-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-select-input': major
---

Add TypeScript types
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Button from '@hashicorp/react-button'
import type { Products } from '@hashicorp/platform-product-meta'
import classNames from 'classnames'
import variantCentered from './styles/variant-centered.module.css'
import variantCompact from './styles/variant-compact.module.css'
Expand All @@ -10,6 +11,20 @@ const stylesDict = {
links: variantLinks,
}

interface CallToActionProps {
heading?: string
content?: string
links?: {
type?: 'inbound' | 'outbound' | 'anchor' | 'download'
text: string
url: string
}[]
variant?: 'centered' | 'compact' | 'links'
product: Products
theme?: 'light' | 'dark' | 'brand'
className?: string
}

function CallToAction({
heading,
content,
Expand All @@ -18,14 +33,16 @@ function CallToAction({
product,
theme = 'light',
className,
}) {
}: CallToActionProps) {
const s = stylesDict[variant]
if (!heading && !content) {
throw new Error('<CallToAction /> requires either heading or content')
}
const hasLinks = links && links.length > 0
if (hasLinks && links.filter((l) => !l.text || !l.url).length > 0) {
throw new Error('<CallToAction /> `links` must have both a "text" and a "url" prop')
throw new Error(
'<CallToAction /> `links` must have both a "text" and a "url" prop'
)
}
return (
<div className={classNames(s.root, s[`theme-${theme}`], className)}>
Expand Down
1 change: 1 addition & 0 deletions packages/call-to-action/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"Zach Shilton"
],
"dependencies": {
"@hashicorp/platform-product-meta": "^0.1.0",
"@hashicorp/react-button": "^6.0.4"
},
"peerDependencies": {
Expand Down
28 changes: 24 additions & 4 deletions packages/code-block/index.js → packages/code-block/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useRef } from 'react'
import type { ReactElement } from 'react'
import classNames from 'classnames'
import processSnippet from './utils/process-snippet'
import ClipboardButton from './partials/clipboard-button'
Expand All @@ -11,6 +12,25 @@ import analytics, { heapAttributes } from './analytics'
import fragment from './fragment.graphql'
import s from './style.module.css'

export interface CodeBlockOptions {
showChrome?: boolean
highlight?: boolean
lineNumbers?: boolean
showClipboard?: boolean
showWindowBar?: boolean
filename?: string
heading?: string
}

export interface CodeBlockProps {
className?: string
code: string | ReactElement
language?: string
theme?: 'light' | 'dark'
hasBarAbove?: boolean
options?: CodeBlockOptions
}

function CodeBlock({
className,
code,
Expand All @@ -24,13 +44,13 @@ function CodeBlock({
showClipboard: false,
showWindowBar: false,
},
}) {
const copyRef = useRef()
}: CodeBlockProps) {
const copyRef = useRef<HTMLPreElement>()

function getText() {
async function getText(): Promise<[null, string] | [unknown, null]> {
try {
// Gather the text content
const rawSnippet = copyRef.current.textContent
const rawSnippet = copyRef.current?.textContent
// Run additional processing, namely for shell commands
const snippet = processSnippet(rawSnippet)
return [null, snippet]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import React from 'react'
import type { ReactElement } from 'react'
import CodeBlock from '..'
import CodeTabs from '../partials/code-tabs'
import type { CodeTabsProps } from '../partials/code-tabs'
import CodeBlockConfig from '../partials/code-block-config'
import type { CodeBlockConfigProps } from '../partials/code-block-config'
import normalizePlainCode from '../utils/normalize-plain-code'
import classNames from 'classnames'
import s from './style.module.css'

const DEFAULT_THEME = 'dark'
const IS_DEV = process.env.NODE_ENV !== 'production'

interface PreProps {
children?: ReactElement[]
className?: string
hasBarAbove?: boolean
theme?: 'light' | 'dark'
}

export function pre({
children,
className,
hasBarAbove,
theme = DEFAULT_THEME,
}) {
}: PreProps) {
// Assert that there is exactly one valid child
const childArray = React.Children.toArray(children)
const childArray = React.Children.toArray(children) as ReactElement[]
if (childArray.length !== 1) {
throw new Error(
`Found <pre> element in MDX with more than one child: ${JSON.stringify(
Expand Down Expand Up @@ -65,11 +75,17 @@ export function pre({
)
}

export function CodeTabsWithMargin({ theme = DEFAULT_THEME, ...props }) {
export function CodeTabsWithMargin({
theme = DEFAULT_THEME,
...props
}: CodeTabsProps) {
return <CodeTabs className={s.codeMargin} {...props} theme={theme} />
}

export function CodeBlockConfigWithMargin({ theme = DEFAULT_THEME, ...rest }) {
export function CodeBlockConfigWithMargin({
theme = DEFAULT_THEME,
...rest
}: CodeBlockConfigProps) {
return (
<CodeBlockConfig
className={classNames({ [s.codeMargin]: !rest.hasBarAbove })}
Expand All @@ -79,15 +95,17 @@ export function CodeBlockConfigWithMargin({ theme = DEFAULT_THEME, ...rest }) {
)
}

export default function codeMdxPrimitives({ theme = DEFAULT_THEME } = {}) {
export default function codeMdxPrimitives({
theme = DEFAULT_THEME,
}: { theme?: 'light' | 'dark' } = {}) {
return {
CodeBlockConfig: function themedCodeBlockConfig(p) {
CodeBlockConfig: function themedCodeBlockConfig(p: CodeBlockConfigProps) {
return CodeBlockConfigWithMargin({ theme, ...p })
},
CodeTabs: function themedCodeTabs(p) {
CodeTabs: function themedCodeTabs(p: CodeTabsProps) {
return CodeTabsWithMargin({ theme, ...p })
},
pre: function themedPre(p) {
pre: function themedPre(p: PreProps) {
return pre({ theme, ...p })
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import svgCopySuccess from './svg/copy-success.svg?include'
import s from './style.module.css'
import analytics, { heapAttributes } from '../../analytics'

function ClipboardButton({ className, getText }) {
interface ClipboardButtonProps {
className?: string
getText: () => Promise<[unknown, null] | [null, string]>
}

function ClipboardButton({ className, getText }: ClipboardButtonProps) {
// copiedState can be null (initial), true (success), or false (failure)
const [copiedState, setCopiedState] = useState()
const [copiedState, setCopiedState] = useState<boolean | null>(null)
// we reset copiedState to its initial value using a timeout
const [resetTimeout, setResetTimeout] = useState()
const [resetTimeout, setResetTimeout] = useState<number>()

// Handle copy button clicks
async function onClick() {
Expand All @@ -20,7 +25,8 @@ function ClipboardButton({ className, getText }) {
// If text cannot be retrieved, exit early to handle the error
if (getTextError) return handleError(getTextError)
// Otherwise, continue on...
const isCopied = await copyToClipboard(text)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const isCopied = copyToClipboard(text!)
// If there's an internal failure copying text, exit early to handle the error
if (!isCopied) return handleError(`ClipboardButton failed. Text: ${text}.`)
// Otherwise, things went well, track the event and set state
Expand All @@ -41,14 +47,14 @@ function ClipboardButton({ className, getText }) {
useEffect(() => {
// Clear any pending timeouts, which can occur if the
// button is quickly clicked multiple times
clearTimeout(resetTimeout)
window.clearTimeout(resetTimeout)
// Only run the copiedState reset if it's needed
const needsReset = copiedState != null
if (needsReset) {
// Let failure messages linger a bit longer
const resetDelay = copiedState == false ? 4000 : 1750
// Set the timeout to reset the copy success state
setResetTimeout(setTimeout(() => setCopiedState(null), resetDelay))
setResetTimeout(window.setTimeout(() => setCopiedState(null), resetDelay))
}
// Clean up if the component unmounts with a pending timeout
return () => clearTimeout(resetTimeout)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import React, { Children } from 'react'
import CodeBlock from '../../index.js'
import type { ReactElement } from 'react'
import CodeBlock from '../../index'
import type { CodeBlockProps, CodeBlockOptions } from '../../index'
import normalizePlainCode from '../../utils/normalize-plain-code'

export interface CodeBlockConfigProps extends CodeBlockProps, CodeBlockOptions {
children?: ReactElement[]
hideClipboard?: boolean
}

function CodeBlockConfig({
className,
children,
Expand All @@ -12,9 +19,9 @@ function CodeBlockConfig({
lineNumbers,
hasBarAbove,
theme,
}) {
}: CodeBlockConfigProps) {
// Ensure there is exactly one valid child element
const validChildren = Children.toArray(children)
const validChildren = Children.toArray(children) as ReactElement[]
const childCount = Children.count(children)
if (childCount !== 1 || validChildren.length !== 1) {
throw new Error(
Expand All @@ -24,7 +31,9 @@ function CodeBlockConfig({
// Validate that the first child is a code block
const onlyChild = validChildren[0]
// TODO: more reliable way to validate childType, not sure how it'll work in production mode with minifying
const childType = onlyChild.props.mdxType || onlyChild.type.name
const childType =
onlyChild.props.mdxType ||
(typeof onlyChild.type === 'string' ? onlyChild.type : onlyChild.type.name)
if (childType !== 'pre' && childType !== 'themedPre') {
throw new Error(
`In CodeBlockConfig, found a child with type "${childType}". Please ensure a fenced code block, which corresponds to the MDX type "pre", is passed to CodeBlockConfig instead. In JSX, please use CodeBlock directly rather than CodeBlockConfig.`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { Children } from 'react'
import type { ReactElement } from 'react'
import classNames from 'classnames'
import resolveTabData from '../../utils/resolve-tab-data'
import useIndexedTabs from '../../provider/use-indexed-tabs'
Expand All @@ -12,8 +13,22 @@ import themeLight from '../../theme-light.module.css'
import s from './style.module.css'
import analytics from '../../analytics'

function CodeTabs({ children, heading, className, tabs, theme = 'dark' }) {
const validChildren = Children.toArray(children)
export interface CodeTabsProps {
children?: ReactElement[]
heading?: string
className?: string
tabs: (string | { group: string; label: string })[]
theme?: 'light' | 'dark'
}

function CodeTabs({
children,
heading,
className,
tabs,
theme = 'dark',
}: CodeTabsProps) {
const validChildren = Children.toArray(children) as ReactElement[]
// Throw an error if the tabs prop is defined, but does not
// match the number of valid children
if (tabs !== undefined && tabs.length !== validChildren.length) {
Expand All @@ -26,7 +41,10 @@ function CodeTabs({ children, heading, className, tabs, theme = 'dark' }) {
const childTypes = validChildren.map((tabChild) => {
let type
// For JSX primitives, the type is captured by the type property
if (typeof tabChild.type == 'string' || typeof tabChild.type == 'number') {
if (
typeof tabChild.type === 'string' ||
typeof tabChild.type === 'number'
) {
type = tabChild.type
// For function components, accept CodeBlock or CodeBlockConfig.
} else if (typeof tabChild.type === 'function') {
Expand All @@ -38,7 +56,9 @@ function CodeTabs({ children, heading, className, tabs, theme = 'dark' }) {
]
const matchIdx = validComponents
.map((c) => c.component)
.indexOf(tabChild.type)
// we use `never` here instead of `any` to signify that we don't care
// about the type
.indexOf(tabChild.type as never)
if (matchIdx >= 0) {
type = validComponents[matchIdx].name
} else {
Expand Down Expand Up @@ -77,7 +97,7 @@ function CodeTabs({ children, heading, className, tabs, theme = 'dark' }) {
const tabGroupIds = parsedTabs.map((t) => t.group)
const [activeTabIdx, setActiveTabIdx] = useIndexedTabs(tabGroupIds)
// Track CodeTab selection with window.analytics
function setActiveTabWithEvent(tabIdx) {
function setActiveTabWithEvent(tabIdx: number) {
analytics.trackTabSelect(tabGroupIds[tabIdx])
setActiveTabIdx(tabIdx)
}
Expand Down
Loading

1 comment on commit 26918b9

@vercel
Copy link

@vercel vercel bot commented on 26918b9 Apr 25, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.