Files
supabase/packages/ui-patterns/src/MultipleCodeBlock/index.tsx
T
Piyush Yadav b8d28b36d9 fix(ui-patterns): prevent duplicate horizontal scrollbar in MultipleCodeBlock (#49940)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Bug fix

## What is the current behavior?

Fixes #49903

In `ConnectSheet` (e.g., Next.js -> Step 2 "Add files"), multi-file
instructions render using `MultipleCodeBlock`.
The `TabsContent` container in
`packages/ui-patterns/src/MultipleCodeBlock/index.tsx` was styled with
`className="... overflow-scroll ..."`.
The CSS property `overflow-scroll` forces both horizontal and vertical
scrollbar tracks to be rendered regardless of whether horizontal content
overflows.
Because the child `<CodeBlock>` already manages its own horizontal
overflow (`overflow-auto`) and fills the container width, `TabsContent`
displayed a frozen/disabled horizontal scrollbar at the top level. When
scrolling down vertically to the bottom of the code snippet,
`<CodeBlock>`'s real horizontal scrollbar came into view, resulting in
inconsistent and duplicate scrollbars.

## What is the new behavior?

- Replaced `overflow-scroll` with `overflow-y-auto` on `TabsContent` in
`MultipleCodeBlock`.
- `TabsContent` cleanly scrolls vertically when content height exceeds
`max-h-72`.
- Eliminates the duplicate/disabled top-level horizontal scrollbar.
- Horizontal code scrolling is cleanly delegated to `<CodeBlock>` only
when lines exceed the available width.
- Added a unit test in
`packages/ui-patterns/src/MultipleCodeBlock/index.test.tsx` verifying
`TabsContent` applies `overflow-y-auto` rather than `overflow-scroll`.

## Additional context

Verified locally:
- `vitest run src/MultipleCodeBlock/index.test.tsx` (all tests passing)
- `tsc --noEmit` in `packages/ui-patterns` (0 type errors)
- `prettier --check` against modified files (passed)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
  * Improved code block scrolling to use vertical scrolling only.
* Prevented unnecessary horizontal scrollbars in multi-file code
examples.
  * Added coverage to verify the updated scrolling behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-03 16:29:36 +10:00

170 lines
3.9 KiB
TypeScript

import { useEffect, useState } from 'react'
import { cn, Tabs, TabsContent, TabsList, TabsTrigger } from 'ui'
import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock'
interface MultipleCodeBlockFile {
name: string
code: string
language?: string
}
interface MultipleCodeBlockProps {
files: MultipleCodeBlockFile[]
value?: string
onValueChange?: (value: string) => void
className?: string
}
const languageAliases: Record<string, CodeBlockLang> = {
bash: 'bash',
csharp: 'csharp',
cs: 'csharp',
curl: 'curl',
dart: 'dart',
go: 'go',
http: 'http',
javascript: 'js',
js: 'js',
json: 'json',
jsx: 'jsx',
kotlin: 'kotlin',
pgsql: 'pgsql',
php: 'php',
py: 'python',
python: 'python',
sh: 'bash',
shell: 'bash',
sql: 'sql',
swift: 'swift',
ts: 'ts',
typescript: 'ts',
yaml: 'yaml',
yml: 'yaml',
}
const extensionLanguageMap: Record<string, CodeBlockLang> = {
astro: 'html',
bash: 'bash',
cjs: 'js',
dart: 'dart',
go: 'go',
js: 'js',
json: 'json',
jsx: 'jsx',
kt: 'kotlin',
mjs: 'js',
php: 'php',
pgsql: 'pgsql',
py: 'python',
sh: 'bash',
sql: 'sql',
swift: 'swift',
svelte: 'html',
ts: 'ts',
vue: 'html',
yaml: 'yaml',
yml: 'yaml',
}
const inferLanguageFromName = (name: string): CodeBlockLang | undefined => {
const lowerName = name.toLowerCase()
if (lowerName.startsWith('.env')) {
return 'bash'
}
const extension = lowerName.split('.').pop()
if (!extension || extension === lowerName) {
return undefined
}
return extensionLanguageMap[extension]
}
const resolveLanguage = (language: string | undefined, name: string): CodeBlockLang => {
if (language) {
const normalized = language.toLowerCase()
const resolved = languageAliases[normalized]
if (resolved) {
return resolved
}
}
return inferLanguageFromName(name) ?? 'js'
}
export const MultipleCodeBlock = ({
files,
value,
onValueChange,
className,
}: MultipleCodeBlockProps) => {
if (!files?.length) {
return null
}
const defaultValue = files[0]?.name ?? ''
const isControlled = value !== undefined
const [internalValue, setInternalValue] = useState(defaultValue)
const trimmedFiles = files.map((file) => ({
...file,
code: typeof file.code === 'string' ? file.code.trim() : file.code,
}))
useEffect(() => {
if (isControlled) return
setInternalValue((currentValue) => {
const currentValueExists = files.some((file) => file.name === currentValue)
return currentValueExists ? currentValue : defaultValue
})
}, [defaultValue, files, isControlled])
const activeValue = isControlled ? value : internalValue
const handleValueChange = (nextValue: string) => {
if (!isControlled) {
setInternalValue(nextValue)
}
onValueChange?.(nextValue)
}
return (
<Tabs
value={activeValue}
onValueChange={handleValueChange}
className={cn('border rounded-lg gap-0 space-y-0 overflow-hidden', className)}
>
<TabsList className="bg-surface-75 px-5 gap-5 overflow-x-auto border-0 border-b">
{files.map((file) => (
<TabsTrigger
key={file.name}
value={file.name}
className="flex items-center gap-1 text-xs px-0 data-[state=active]:bg-transparent py-2.5"
>
{file.name}
</TabsTrigger>
))}
</TabsList>
{trimmedFiles.map((file) => (
<TabsContent
key={file.name}
value={file.name}
forceMount
className="p-0 max-h-72 overflow-y-auto data-[state=inactive]:hidden"
data-connect-tab-content
data-tab-label={file.name}
>
<CodeBlock
value={typeof file.code === 'string' ? file.code.trim() : file.code}
language={resolveLanguage(file.language, file.name)}
className="min-h-72 !bg-surface-75 rounded-none border-0"
/>
</TabsContent>
))}
</Tabs>
)
}