NickyMeulemanNime
Metadata
  • Date

  • Last update

  • By

    • Nicky Meuleman
  • Tagged

    • React
    • mdx
  • Older post

    Adding math support to a Gatsby MDX blog

  • Newer post

    CSS Position

Table of contents
  1. Proof of concept
  2. Codeblocks
    1. Tab labels
    2. Buttons inside the codeblock

Multilingual codeblocks

Multilingual codeblocks

Proof of concept

I started off simple, and I use that word because I used a package that provides a tab component in React.

Making one myself was a lot more complex than anticipated I had anticipated, so thank you @reach/tabs.

My first attempt were 3 tabs with hardcoded labels, very fragile, but perfect as proof of concept.

MultiLangCodeBlock.js
import { Tabs, TabList, Tab, TabPanels, TabPanel } from "@reach/tabs";
import "@reach/tabs/styles.css";
const MultiLangCodeBlock = (props) => {
return (
<Tabs>
<TabList>
<Tab>1</Tab>
<Tab>2</Tab>
<Tab>3</Tab>
</TabList>
<TabPanels>
<TabPanel>{props.children[0]}</TabPanel>
<TabPanel>{props.children[1]}</TabPanel>
<TabPanel>{props.children[2]}</TabPanel>
</TabPanels>
</Tabs>
);
};

I can import that component into a .mdx blogpost and pass it an array of 3 items to be rendered.

I can even pass a React component as an array item, it’s great.

blogpost.mdx
import { MultiLangCodeBlock } from "./src/components/MultiLangCodeBlock";
import { Hobbits } from "./src/components/Hobbits";
<MultiLangCodeBlock>
{["They're taking the", "Hobbits to Isengard!", <Hobbits />]}
</MultiLangCodeBlock>

The result can be seen below. Be sure to check out all tabs.

They're taking the
Hobbits to Isengard!

Codeblocks

Let’s try to pass a codeblock as an item in that array.

const language = "JavaScript";
language = "Python"
let language = "Rust";

That worked!

Be sure to pass those codeblocks as markdown, not as a literal JavaScript array.

blogpost.mdx
<MultiLangCodeBlock>
```js
const language = "JavaScript";
```
```python
language = "Python"
```
```rust
let language = "Rust";
```
</MultiLangCodeBlock>

Tab labels

I replaced the hardcoded amount of tabs, and made it so the tab labels were the language from the triple backtick codeblock.

Using my single language CodeBlock component means the fancy options (like highlighting specific lines) work too!

MultiLangCodeBlock.js
const MultiLangCodeBlock = (props) => {
return (
<Tabs>
<TabList>
{props.children.map((child) => {
const [language] = child.props.children.props.className
.replace(/language-/, ``)
.split(` `);
return <Tab>{language}</Tab>;
})}
</TabList>
<TabPanels>
{props.children.map((child) => {
return <TabPanel>{child}</TabPanel>;
})}
</TabPanels>
</Tabs>
);
};

This mdx will generate the output underneath this block, neat!

blogpost.mdx
<MultiLangCodeBlock>
```js title=index.js numberLines hl=1
const language = "JavaScript";
console.log(language);
```
```python title=index.py numberLines hl=1
language = "Python"
print(language)
```
```rust title=index.rs numberLines hl=1
let language = "Rust";
println!("{}", language);
```
</MultiLangCodeBlock>
index.js
1const language = "JavaScript";
2
3console.log(language);
index.py
1language = "Python"
2
3print(language)
index.rs
1let language = "Rust";
2
3println!("{}", language);

This doesn’t look too bad.

Buttons inside the codeblock

I wanted the tab buttons to be inside the header of a codeblock, to the right of the title.

The tab buttons being inside the codeblock was a problem. That meant the button for a tab would be inside the content for a tab, that doesn’t sound right.

I decided to manually render my single language CodeBlock component without a top part. The CodeBlock component would no longer be responsible for that header, the MultiLangCodeBlock would take on that responsability.

The title option is what causes the single language component to have a header. So I prevented that option from reaching the single language block by not passing that title as a prop.

I still want the title to be displayed however, so I have to use that title information in the new MultiLangCodeBlock.

That meant I needed a bit of state management to keep track of the current tab index, the title, and the label for that tab.

The MultiLangCodeBlock looked great after a little CSS to display the tab buttons to the right of the title and make that entire line look like the top of my single language codeblocks.

MultiLangCodeBlock.js
const MultiLangCodeBlock = ({ children }) => {
const codeTitles = children.map(
(child) => child?.props?.children?.props?.title
);
const tabLabels = children.map((child) =>
child?.props?.children?.props?.className.replace(/language-/, ``).split(` `)
);
const [tabIndex, setTabindex] = useState(0);
const [title, setTitle] = useState(codeTitles[0]);
const handleTabsChange = (index) => {
setTabindex(index);
setTitle(codeTitles[index]);
};
return (
<Tabs index={tabIndex} onChange={handleTabsChange}>
<div sx={{ display: "flex", variant: `styles.CodeBlock.title` }}>
<div sx={{ flex: 1 }}>{title}</div>
<TabList
sx={{
color: "mutedText",
"[data-selected]": { color: "mutedPrimary" },
}}
>
{tabLabels.map((label) => (
<Tab key={label}>{label}</Tab>
))}
</TabList>
</div>
<TabPanels>
{children.map((child) => {
// split off title so the CodeBlock from the theme doesn't render a header, this component does that
const { title, ...blockProps } = child.props.children.props;
return (
<TabPanel key={blockProps.className}>
<CodeBlock {...blockProps} />
</TabPanel>
);
})}
</TabPanels>
</Tabs>
);
};

What’s in the .mdx file:

mypost.mdx
<TabsInTitle>
```js title=index.js numberLines hl=1
const language = "JavaScript";
console.log(language);
```
```python title=index.py numberLines hl=1
language = "Python"
print(language)
```
```rust title=index.rs numberLines hl=1
let language = "Rust";
println!("{}", language);
```
</TabsInTitle>

The resulting output:

index.js
1const language = "JavaScript";
2
3console.log(language);
1language = "Python"
2
3print(language)
1let language = "Rust";
2
3println!("{}", language);

Designed and developed by Nicky Meuleman

Built with Gatsby. Hosted on Netlify.