Skip to content

Commit d73d7bb

Browse files
authored
feat: make code block header action buttons sticky (copy/download) (#401)
* refactor: simplify CodeBlockHeader component by removing unused children prop * fix: remove overflow-hidden class from CodeBlockContainer * feat: add sticky button for code block actions * fix: update selector for code block action buttons in tests * feat: add sticky action buttons to code block header for improved accessibility
1 parent cfd16ea commit d73d7bb

File tree

5 files changed

+26
-16
lines changed

5 files changed

+26
-16
lines changed

.changeset/happy-taxes-double.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"streamdown": minor
3+
---
4+
5+
Make the action buttons in code block header sticky.
6+
Ensures copy buttons remain accessible for long code blocks.
7+
Improves usability when viewing large snippets.

packages/streamdown/__tests__/show-controls.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ graph TD
6868
);
6969

7070
const buttons = container.querySelectorAll(
71-
'[data-streamdown="code-block-header"] button'
71+
'[data-streamdown="code-block-actions"] button'
7272
);
7373

7474
expect(buttons?.length).toBe(0);
@@ -108,7 +108,7 @@ graph TD
108108
);
109109

110110
const buttons = container.querySelectorAll(
111-
'[data-streamdown="code-block-header"] button'
111+
'[data-streamdown="code-block-actions"] button'
112112
);
113113

114114
expect(buttons?.length).toBe(0);
@@ -121,7 +121,7 @@ graph TD
121121

122122
await waitFor(() => {
123123
const buttons = container.querySelectorAll(
124-
'[data-streamdown="code-block-header"] button'
124+
'[data-streamdown="code-block-actions"] button'
125125
);
126126
expect(buttons?.length).toBeGreaterThan(0);
127127
});
@@ -183,7 +183,7 @@ ${markdownWithCode}
183183

184184
await waitFor(() => {
185185
const codeButtons = container.querySelectorAll(
186-
'[data-streamdown="code-block-header"] button'
186+
'[data-streamdown="code-block-actions"] button'
187187
);
188188
expect(codeButtons?.length).toBeGreaterThan(0);
189189
});
@@ -208,7 +208,7 @@ ${markdownWithCode}
208208
// Code controls should still show since not specified
209209
await waitFor(() => {
210210
const codeButtons = container.querySelectorAll(
211-
'[data-streamdown="code-block-header"] button'
211+
'[data-streamdown="code-block-actions"] button'
212212
);
213213
expect(codeButtons?.length).toBeGreaterThan(0);
214214
});

packages/streamdown/lib/code-block/container.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const CodeBlockContainer = ({
1616
}: CodeBlockContainerProps) => (
1717
<div
1818
className={cn(
19-
"my-4 w-full overflow-hidden rounded-xl border border-border",
19+
"my-4 w-full rounded-xl border border-border",
2020
className
2121
)}
2222
data-incomplete={isIncomplete || undefined}
Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
1-
import type { ReactNode } from "react";
2-
31
interface CodeBlockHeaderProps {
42
language: string;
5-
children: ReactNode;
63
}
74

8-
export const CodeBlockHeader = ({
9-
language,
10-
children,
11-
}: CodeBlockHeaderProps) => (
5+
export const CodeBlockHeader = ({ language }: CodeBlockHeaderProps) => (
126
<div
13-
className="flex items-center justify-between bg-muted/80 p-3 text-muted-foreground text-xs"
7+
className="flex h-12 items-center bg-muted/80 px-4 text-muted-foreground text-xs"
148
data-language={language}
159
data-streamdown="code-block-header"
1610
>
1711
<span className="ml-1 font-mono lowercase">{language}</span>
18-
<div className="flex items-center gap-2">{children}</div>
1912
</div>
2013
);

packages/streamdown/lib/code-block/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,17 @@ export const CodeBlock = ({
5555
return (
5656
<CodeBlockContext.Provider value={{ code }}>
5757
<CodeBlockContainer isIncomplete={isIncomplete} language={language}>
58-
<CodeBlockHeader language={language}>{children}</CodeBlockHeader>
58+
<CodeBlockHeader language={language} />
59+
{children ? (
60+
<div className="pointer-events-none sticky top-0 z-10 -mt-12 flex h-12 items-center justify-end px-4">
61+
<div
62+
className="pointer-events-auto flex shrink-0 items-center gap-2 rounded-md bg-muted/80 px-1.5 py-1 supports-[backdrop-filter]:bg-muted/70 supports-[backdrop-filter]:backdrop-blur"
63+
data-streamdown="code-block-actions"
64+
>
65+
{children}
66+
</div>
67+
</div>
68+
) : null}
5969
<Suspense
6070
fallback={
6171
<CodeBlockBody

0 commit comments

Comments
 (0)