mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 12:25:05 +03:00
## 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 -->
170 lines
3.9 KiB
TypeScript
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>
|
|
)
|
|
}
|