Skip to content

Commit

Permalink
add Toggle Word Wrap button for code-blocks (only for mobile) (#771)
Browse files Browse the repository at this point in the history
* add `Toggle Word Wrap` button for code-blocks (only for mobile)

* simplify

* fix missing `Copy Code` button in code-blocks without language (#774)

* fix missing `Copy Code` button in code-blocks without language

* add test example

* update snapshots
  • Loading branch information
dimaMachina committed Sep 2, 2022
1 parent e2d603a commit ff8967c
Show file tree
Hide file tree
Showing 28 changed files with 213 additions and 127 deletions.
7 changes: 7 additions & 0 deletions .changeset/ninety-singers-run.md
@@ -0,0 +1,7 @@
---
'nextra': patch
'nextra-theme-blog': patch
'nextra-theme-docs': patch
---

add `Toggle Word Wrap` button for code-blocks (only for mobile)
7 changes: 7 additions & 0 deletions .changeset/pretty-dolphins-live.md
@@ -0,0 +1,7 @@
---
'nextra': patch
'nextra-theme-blog': patch
'nextra-theme-docs': patch
---

fix missing `Copy Code` button in code-blocks without language
@@ -0,0 +1,5 @@
# Code blocks without language have `Copy Code` button

```
hello world
```
7 changes: 3 additions & 4 deletions packages/nextra-theme-blog/src/theme-switch.tsx
@@ -1,12 +1,11 @@
import React, { useState, useEffect } from 'react'
import React from 'react'
import { useTheme } from 'next-themes'
import { MoonIcon, SunIcon } from 'nextra/icons'
import { useMounted } from 'nextra/hooks'

export default function ThemeSwitch() {
const { theme, setTheme, resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)

useEffect(() => setMounted(true), [])
const mounted = useMounted()
const isDark = theme === 'dark' || resolvedTheme === 'dark'

// @TODO: system theme
Expand Down
3 changes: 2 additions & 1 deletion packages/nextra-theme-docs/src/components/head.tsx
@@ -1,7 +1,8 @@
import React, { ReactElement } from 'react'
import NextHead from 'next/head'
import { useTheme } from 'next-themes'
import { renderString, useMounted } from '../utils'
import { useMounted } from 'nextra/hooks'
import { renderString } from '../utils'
import { useConfig } from '../contexts'

export function Head(): ReactElement {
Expand Down
3 changes: 2 additions & 1 deletion packages/nextra-theme-docs/src/components/not-found.tsx
@@ -1,6 +1,7 @@
import React, { ReactElement } from 'react'
import { useMounted } from 'nextra/hooks'
import { useConfig } from '../contexts'
import { renderComponent, useMounted, getGitIssueUrl } from '../utils'
import { renderComponent, getGitIssueUrl } from '../utils'
import { useRouter } from 'next/router'
import { Anchor } from './anchor'

Expand Down
3 changes: 2 additions & 1 deletion packages/nextra-theme-docs/src/components/search.tsx
Expand Up @@ -10,9 +10,10 @@ import React, {
import cn from 'clsx'
import { Transition } from '@headlessui/react'
import { SpinnerIcon } from 'nextra/icons'
import { useMounted } from 'nextra/hooks'
import { Input } from './input'
import { Anchor } from './anchor'
import { renderComponent, renderString, useMounted } from '../utils'
import { renderComponent, renderString } from '../utils'
import { useConfig, useMenu } from '../contexts'
import { useRouter } from 'next/router'
import { SearchResult } from '../types'
Expand Down
@@ -1,6 +1,7 @@
import React, { ReactElement } from 'react'
import { useMounted } from 'nextra/hooks'
import { useConfig } from '../contexts'
import { renderComponent, useMounted, getGitIssueUrl } from '../utils'
import { renderComponent, getGitIssueUrl } from '../utils'
import { useRouter } from 'next/router'
import { Anchor } from './anchor'

Expand Down
2 changes: 1 addition & 1 deletion packages/nextra-theme-docs/src/components/theme-switch.tsx
Expand Up @@ -2,7 +2,7 @@ import React, { memo, ReactElement } from 'react'
import { useTheme } from 'next-themes'
import { Select } from './select'
import { SunIcon, MoonIcon } from 'nextra/icons'
import { useMounted } from '../utils'
import { useMounted } from 'nextra/hooks'
import { useConfig } from '../contexts'

export function ThemeSwitch({ lite = true }): ReactElement {
Expand Down
1 change: 0 additions & 1 deletion packages/nextra-theme-docs/src/utils/index.ts
Expand Up @@ -4,4 +4,3 @@ export { getGitIssueUrl } from './get-git-issue-url'
export { getHeadingText } from './get-heading-text'
export { Item, PageItem, MenuItem, normalizePages } from './normalize-pages'
export { renderComponent, renderString } from './render'
export { useMounted } from './use-mounted'
12 changes: 12 additions & 0 deletions packages/nextra/__test__/__snapshots__/context.test.ts.snap
Expand Up @@ -1139,6 +1139,12 @@ exports[`context > getAllPages() > should work 1`] = `
"name": "raw-layout",
"route": "/docs/raw-layout",
},
{
"locale": "en-US",
"meta": undefined,
"name": "code-block-without-language",
"route": "/docs/code-block-without-language",
},
],
"meta": {
"theme": {
Expand Down Expand Up @@ -2677,6 +2683,12 @@ exports[`context > getCurrentLevelPages() > should work 1`] = `
"name": "raw-layout",
"route": "/docs/raw-layout",
},
{
"locale": "en-US",
"meta": undefined,
"name": "code-block-without-language",
"route": "/docs/code-block-without-language",
},
]
`;

Expand Down
10 changes: 10 additions & 0 deletions packages/nextra/__test__/__snapshots__/page-map.test.ts.snap
Expand Up @@ -304,6 +304,11 @@ exports[`Page Process > pageMap en-US 1`] = `
"name": "change-log",
"route": "/docs/change-log",
},
{
"locale": "en-US",
"name": "code-block-without-language",
"route": "/docs/code-block-without-language",
},
{
"locale": "en-US",
"name": "conditional-fetching",
Expand Down Expand Up @@ -734,6 +739,11 @@ exports[`Page Process > pageMap zh-CN 1`] = `
"name": "advanced",
"route": "/docs/advanced",
},
{
"locale": "en-US",
"name": "code-block-without-language",
"route": "/docs/code-block-without-language",
},
{
"locale": "en-US",
"name": "raw-layout",
Expand Down
8 changes: 7 additions & 1 deletion packages/nextra/package.json
Expand Up @@ -32,6 +32,9 @@
],
"components": [
"./dist/components/index.d.ts"
],
"hooks": [
"./dist/hooks/index.d.ts"
]
}
},
Expand All @@ -51,6 +54,10 @@
"import": "./dist/components/index.mjs",
"types": "./dist/components/index.d.ts"
},
"./hooks": {
"import": "./dist/hooks/index.mjs",
"types": "./dist/hooks/index.d.ts"
},
"./*": {
"import": "./dist/*.mjs",
"types": "./dist/*.d.ts"
Expand All @@ -74,7 +81,6 @@
"github-slugger": "^1.4.0",
"graceful-fs": "^4.2.10",
"gray-matter": "^4.0.3",
"react-children-utilities": "^2.8.0",
"rehype-mdx-title": "^1.0.0",
"rehype-pretty-code": "0.2.4",
"remark-gfm": "^3.0.1",
Expand Down
10 changes: 5 additions & 5 deletions packages/nextra/src/compile.ts
Expand Up @@ -66,16 +66,16 @@ export async function compileMdx(
].filter(truthy),
rehypePlugins: [
...(mdxOptions.rehypePlugins || []),
[
parseMeta,
{ defaultShowCopyCode: nextraOptions.unstable_defaultShowCopyCode }
],
parseMeta,
[
rehypePrettyCode,
{ ...rehypePrettyCodeOptions, ...mdxOptions.rehypePrettyCodeOptions }
],
[rehypeMdxTitle, { name: '__nextra_title__' }],
attachMeta
[
attachMeta,
{ defaultShowCopyCode: nextraOptions.unstable_defaultShowCopyCode }
]
]
})
try {
Expand Down
21 changes: 21 additions & 0 deletions packages/nextra/src/components/button.tsx
@@ -0,0 +1,21 @@
import React, { ComponentProps, ReactElement } from 'react'

export const Button = ({
children,
className = '',
...props
}: ComponentProps<'button'>): ReactElement => {
return (
<button
className={[
'nextra-button transition-colors',
'bg-primary-700/5 border border-black/5 text-gray-600 hover:text-gray-900 rounded-md p-2',
'dark:bg-primary-300/10 dark:border-white/10 dark:text-gray-400 dark:hover:text-gray-50',
className
].join(' ')}
{...props}
>
{children}
</button>
)
}
15 changes: 13 additions & 2 deletions packages/nextra/src/components/code.tsx
Expand Up @@ -2,11 +2,22 @@ import React, { ComponentProps, ReactElement } from 'react'

export const Code = ({
children,
className = '',
...props
}: ComponentProps<'code'>): ReactElement => {
const hasLineNumbers = 'data-line-numbers' in props
return (
// always show code blocks in ltr
<code dir="ltr" {...props}>
<code
className={[
'border-black/5 bg-black/5 break-words rounded-md border py-0.5 px-[.25em] text-[.9em]',
'dark:border-white/10 dark:bg-white/10',
hasLineNumbers ? '[counter-reset:line]' : '',
className
].join(' ')}
// always show code blocks in ltr
dir="ltr"
{...props}
>
{children}
</code>
)
Expand Down
19 changes: 5 additions & 14 deletions packages/nextra/src/components/copy-to-clipboard.tsx
@@ -1,19 +1,18 @@
import React, {
ComponentProps,
ReactElement,
ReactNode,
useCallback,
useEffect,
useState
} from 'react'
import { onlyText } from 'react-children-utilities'
import { CheckIcon, CopyIcon } from '../icons'
import { Button } from './button'

export const CopyToClipboard = ({
value,
className
}: {
value: ReactNode
value: string
className?: string
}): ReactElement => {
const [isCopied, setCopied] = useState(false)
Expand All @@ -37,7 +36,7 @@ export const CopyToClipboard = ({
console.error('Access to clipboard rejected!')
}
try {
await navigator.clipboard.writeText(onlyText(value))
await navigator.clipboard.writeText(JSON.parse(value))
} catch {
console.error('Failed to copy!')
}
Expand All @@ -46,16 +45,8 @@ export const CopyToClipboard = ({
const IconToUse = isCopied ? CheckIcon : CopyIcon

return (
<button
onClick={handleClick}
className={[
'nextra-copy-button rounded-md p-2 transition-colors',
'bg-primary-700/5 dark:bg-primary-300/10 border border-black/5 dark:border-white/10',
'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50',
className || ''
].join(' ')}
>
<Button onClick={handleClick} className={className}>
<IconToUse className="pointer-events-none h-4 w-4" />
</button>
</Button>
)
}
1 change: 1 addition & 0 deletions packages/nextra/src/components/index.ts
@@ -1,3 +1,4 @@
export { Button } from './button'
export { CopyToClipboard } from './copy-to-clipboard'
export { Code } from './code'
export { Pre } from './pre'
Expand Down
45 changes: 32 additions & 13 deletions packages/nextra/src/components/pre.tsx
@@ -1,16 +1,27 @@
import React, { ComponentProps, ReactElement } from 'react'
import React, { ComponentProps, ReactElement, useCallback } from 'react'
import { CopyToClipboard } from './copy-to-clipboard'
import { Button } from './button'
import { WordWrapIcon } from '../icons'

export const Pre = ({
children,
className,
className = '',
value,
filename,
...props
}: ComponentProps<'pre'> & {
'data-filename'?: string
'data-nextra-copy'?: ''
filename?: string
value?: string
}): ReactElement => {
const hasCopy = 'data-nextra-copy' in props
const filename = props['data-filename']
const toggleWordWrap = useCallback(() => {
const htmlDataset = document.documentElement.dataset
const hasWordWrap = 'nextraWordWrap' in htmlDataset
if (hasWordWrap) {
delete htmlDataset.nextraWordWrap
} else {
htmlDataset.nextraWordWrap = ''
}
}, [])

return (
<>
Expand All @@ -23,18 +34,26 @@ export const Pre = ({
className={[
'bg-primary-700/5 mt-6 mb-4 overflow-x-auto rounded-xl font-medium subpixel-antialiased dark:bg-primary-300/10',
filename ? 'pt-12 pb-4' : 'py-4',
className || ''
className
].join(' ')}
{...props}
>
{children}
</pre>
{hasCopy && (
<CopyToClipboard
value={children}
className={'opacity-0 transition-opacity absolute m-2 right-0 ' + (filename ? 'top-8' : 'top-0')}
/>
)}
<div
className={[
'nextra-code-block-buttons opacity-0 transition-opacity [div:hover>&]:opacity-100',
'flex gap-1 absolute m-2 right-0',
filename ? 'top-8' : 'top-0'
].join(' ')}
>
<Button onClick={toggleWordWrap} className="md:hidden">
<WordWrapIcon className="pointer-events-none w-4 h-4" />
</Button>
{value && (
<CopyToClipboard value={value} className="nextra-copy-button" />
)}
</div>
</>
)
}
1 change: 1 addition & 0 deletions packages/nextra/src/hooks/index.ts
@@ -0,0 +1 @@
export * from './use-mounted'
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'

export function useMounted() {
export function useMounted(): boolean {
const [mounted, setMounted] = useState(false)

useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions packages/nextra/src/icons/index.ts
Expand Up @@ -8,4 +8,5 @@ export * from './menu'
export * from './moon'
export * from './spinner'
export * from './sun'
export * from './word-wrap'
export * from './x'
12 changes: 12 additions & 0 deletions packages/nextra/src/icons/word-wrap.tsx
@@ -0,0 +1,12 @@
import React, { ComponentProps, ReactElement } from 'react'

export const WordWrapIcon = (props: ComponentProps<'svg'>): ReactElement => {
return (
<svg viewBox="0 0 24 24" width="24" height="24" {...props}>
<path
fill="currentColor"
d="M4 19h6v-2H4v2zM20 5H4v2h16V5zm-3 6H4v2h13.25c1.1 0 2 .9 2 2s-.9 2-2 2H15v-2l-3 3l3 3v-2h2c2.21 0 4-1.79 4-4s-1.79-4-4-4z"
/>
</svg>
)
}

0 comments on commit ff8967c

Please sign in to comment.