Chore/migrate modals 1 (#46216)

## Problem

We still use the deprecated `Modal` component in many places.

## Solution

Migrate to the new `Dialog` component.
Removed the design system page that was missing anyway:
https://supabase.com/design-system/docs/fragments/modal
This PR focuses on the design system and docs related components.

## How to test

- Docs videos: 
  - prod: https://supabase.com/docs/guides/auth
- staging:
https://docs-git-chore-migrate-modals-1-supabase.vercel.app/docs/guides/auth

- Feedback modal: not sure how to show it on staging but it should look
identical
- Privacy settings: not sure how to show it on staging but it should
look identical
- `PopupFrame`: this one is not used anywhere AFAIK. If confirmed I
could delete it.

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

## Summary by CodeRabbit

## Release Notes

* **Chores**
* Removed Modal component documentation entries and example patterns
from the design system.
* Migrated internal modal and dialog implementations to use the Dialog
component suite for standardized behavior and improved consistency
across the application.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46216?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Gildas Garcia authored and GitHub committed 2026-05-21 15:30:03 +02:00
1 parent 57b7aa2f3d
commit 31c8601201
10 files changed
+201 -441

No files matched your search

-5
View File
@@ -132,11 +132,6 @@ export const docsConfig: DocsConfig = {
href: '/docs/fragments/empty-state-presentational',
items: [],
},
{
title: 'Modal',
href: '/docs/fragments/modal',
items: [],
},
{
title: 'Page Container',
href: '/docs/fragments/page-container',
@@ -1,43 +0,0 @@
import { Link2 } from 'lucide-react'
import { useState } from 'react'
import { Button, Modal } from 'ui'
export default function ModalVerticalAlignedFooter() {
const [visible, setVisible] = useState(false)
return (
<>
<Button type="default" onClick={() => setVisible(!visible)}>
Open Modal
</Button>
<Modal
visible={visible}
onCancel={() => setVisible(!visible)}
onConfirm={() => setVisible(!visible)}
title="This is the title of the modal"
description="And i am the description"
alignFooter="right"
size="medium"
hideClose={false}
header={
<div className="flex items-center gap-2 text-foreground">
<div className="text-brand">
<Link2 />
</div>
<div className="flex items-baseline gap-2">
<h3>This is the title</h3>
<span className="text-xs text-foreground-muted">This is the title</span>
</div>
</div>
}
>
<Modal.Content>
<p>
Modal content is inserted here, if you need to insert anything into the Modal you can do
so via `children`.
</p>
</Modal.Content>
</Modal>
</>
)
}
@@ -1,51 +0,0 @@
import { Link2 } from 'lucide-react'
import { useState } from 'react'
import { Badge, Button, Modal } from 'ui'
export default function ModalVerticalCustomFooter() {
const [visible, setVisible] = useState(false)
return (
<>
<Button type="default" onClick={() => setVisible(!visible)}>
Open Modal
</Button>
<Modal
visible={visible}
onCancel={() => setVisible(!visible)}
onConfirm={() => setVisible(!visible)}
title="This is the title of the modal"
description="And i am the description"
size="medium"
hideClose={false}
header={
<div className="flex items-center gap-2 text-foreground">
<div className="text-brand">
<Link2 />
</div>
<div className="flex items-baseline gap-2">
<h3>This is the title</h3>
<span className="text-xs text-foreground-muted">This is the title</span>
</div>
</div>
}
customFooter={
<div className="flex items-center gap-3">
<div>
<Badge variant="destructive">Proceed with caution</Badge>
</div>
<Button type="secondary">Cancel</Button>
<Button type="danger">Delete</Button>
</div>
}
>
<Modal.Content>
<p>
Modal content is inserted here, if you need to insert anything into the Modal you can do
so via `children`.
</p>
</Modal.Content>
</Modal>
</>
)
}
@@ -1,43 +0,0 @@
import { Link2 } from 'lucide-react'
import { useState } from 'react'
import { Button, Modal } from 'ui'
export default function ModalVerticalHideFooter() {
const [visible, setVisible] = useState(false)
return (
<>
<Button type="default" onClick={() => setVisible(!visible)}>
Open Modal
</Button>
<Modal
visible={visible}
onCancel={() => setVisible(!visible)}
onConfirm={() => setVisible(!visible)}
title="This is the title of the modal"
description="And i am the description"
hideFooter={true}
size="medium"
hideClose={false}
header={
<div className="flex items-center gap-2 text-foreground">
<div className="text-brand">
<Link2 />
</div>
<div className="flex items-baseline gap-2">
<h3>This is the title</h3>
<span className="text-xs text-foreground-muted">This is the title</span>
</div>
</div>
}
>
<Modal.Content>
<p>
Modal content is inserted here, if you need to insert anything into the Modal you can do
so via `children`.
</p>
</Modal.Content>
</Modal>
</>
)
}
@@ -1,43 +0,0 @@
import { Link2 } from 'lucide-react'
import { useState } from 'react'
import { Button, Modal } from 'ui'
export default function ModalVerticalLayout() {
const [visible, setVisible] = useState(false)
return (
<>
<Button type="default" onClick={() => setVisible(!visible)}>
Open Modal
</Button>
<Modal
visible={visible}
onCancel={() => setVisible(!visible)}
onConfirm={() => setVisible(!visible)}
title="This is the title of the modal"
description="And i am the description"
layout="vertical"
size="medium"
hideClose={false}
header={
<div className="flex items-center gap-2 text-foreground">
<div className="text-brand">
<Link2 />
</div>
<div className="flex items-baseline gap-2">
<h3>This is the title</h3>
<span className="text-xs text-foreground-muted">This is the title</span>
</div>
</div>
}
>
<Modal.Content>
<p>
Modal content is inserted here, if you need to insert anything into the Modal you can do
so via `children`.
</p>
</Modal.Content>
</Modal>
</>
)
}
+7 -8
View File
@@ -1,25 +1,24 @@
'use client'
import { createClient } from '@supabase/supabase-js'
import { IS_PLATFORM } from '~/lib/constants'
import { useSendFeedbackMutation } from '~/lib/fetch/feedback'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { useConstant, useIsLoggedIn, type Database } from 'common'
import { Check, MessageSquareQuote, X } from 'lucide-react'
import { usePathname } from 'next/navigation'
import {
type CSSProperties,
type MouseEventHandler,
forwardRef,
useReducer,
useRef,
useState,
type CSSProperties,
type MouseEventHandler,
} from 'react'
import { type Database, useConstant, useIsLoggedIn } from 'common'
import { Button, cn } from 'ui'
import { IS_PLATFORM } from '~/lib/constants'
import { useSendFeedbackMutation } from '~/lib/fetch/feedback'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { getLinearTeam, getSanitizedTabParams } from './Feedback.utils'
import { type FeedbackFields, FeedbackModal } from './FeedbackModal'
import { FeedbackModal, type FeedbackFields } from './FeedbackModal'
const FeedbackButton = forwardRef<
HTMLButtonElement,
+78 -69
View File
@@ -1,6 +1,20 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { SubmitHandler, useForm } from 'react-hook-form'
import { Button, Form, FormControl, FormField, Input, Modal, Textarea } from 'ui'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
Form,
FormControl,
FormField,
Input,
Textarea,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'
@@ -24,6 +38,7 @@ function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps
defaultValues: { page, title: '', comment: '' },
resolver: zodResolver(formSchema),
})
const formId = 'feedback-form'
const { reset } = form
const { isSubmitting } = form.formState
@@ -38,75 +53,69 @@ function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps
}
return (
<Modal
hideFooter
header="Leave a comment"
visible={visible}
onCancel={handleCancel}
onEscapeKeyDown={handleCancel}
>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
<Modal.Content className="pt-4 pb-2 flex flex-col gap-2">
<input type="hidden" id="page" {...form.register('page')} />
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItemLayout layout="vertical" label="Title">
<FormControl className="col-span-6">
<Input {...field} />
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="comment"
render={({ field }) => (
<FormItemLayout layout="vertical" label="Comment" afterLabel="(not anonymous)">
<FormControl className="col-span-6">
<Textarea {...field} rows={4} className="resize-none" />
</FormControl>
</FormItemLayout>
)}
/>
<div className="flex gap-2 text-xs text-foreground-light leading-relaxed">
<span className="shrink-0 mt-0.5">💡</span>
<div>
<strong>Need help or support?</strong> This feedback form is for documentation
improvements only. For technical support, please submit a{' '}
<a
href="https://supabase.com/dashboard/support/new"
target="_blank"
rel="noopener noreferrer"
className="text-brand-link hover:underline"
>
support request
</a>
.
<Dialog open={visible} onOpenChange={() => onCancel()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Leave a comment</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<Form {...form}>
<form id={formId} onSubmit={form.handleSubmit(handleSubmit)}>
<DialogSection className="space-y-4">
<input type="hidden" id="page" {...form.register('page')} />
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItemLayout layout="vertical" label="Title">
<FormControl className="col-span-6">
<Input {...field} />
</FormControl>
</FormItemLayout>
)}
/>
<FormField
control={form.control}
name="comment"
render={({ field }) => (
<FormItemLayout layout="vertical" label="Comment" afterLabel="(not anonymous)">
<FormControl className="col-span-6">
<Textarea {...field} rows={4} className="resize-none" />
</FormControl>
</FormItemLayout>
)}
/>
<div className="flex gap-2 text-xs text-foreground-light leading-relaxed">
<span className="shrink-0 mt-0.5">💡</span>
<div>
<strong>Need help or support?</strong> This feedback form is for documentation
improvements only. For technical support, please submit a{' '}
<a
href="https://supabase.com/dashboard/support/new"
target="_blank"
rel="noopener noreferrer"
className="text-brand-link hover:underline"
>
support request
</a>
.
</div>
</div>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content className="pt-2 pb-4">
<div className="flex items-center justify-end gap-2">
<Button
htmlType="reset"
type="default"
onClick={handleCancel}
disabled={isSubmitting}
>
Cancel
</Button>
<Button htmlType="submit" loading={isSubmitting} disabled={isSubmitting}>
Submit feedback
</Button>
</div>
</Modal.Content>
</form>
</Form>
</Modal>
</DialogSection>
</form>
</Form>
<DialogFooter>
<div className="flex items-center justify-end gap-2">
<Button htmlType="reset" type="default" onClick={handleCancel} disabled={isSubmitting}>
Cancel
</Button>
<Button htmlType="submit" form={formId} loading={isSubmitting} disabled={isSubmitting}>
Submit feedback
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -2,7 +2,7 @@ import { useBreakpoint } from 'common'
import { Play } from 'lucide-react'
import Image from 'next/image'
import React, { ReactNode } from 'react'
import { cn, Modal } from 'ui'
import { Dialog, DialogContent, DialogTrigger } from 'ui'
interface ExpandableVideoProps {
videoId: string
@@ -28,22 +28,6 @@ export function ExpandableVideo({
const [expandVideo, setExpandVideo] = React.useState(false)
const isMobile = useBreakpoint(768)
React.useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
switch (e.key) {
case 'Escape':
return setExpandVideo(false)
default:
return
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [])
React.useEffect(() => {
if (isMobile) setExpandVideo(false)
}, [isMobile])
@@ -83,51 +67,38 @@ export function ExpandableVideo({
return (
<>
<Modal
visible={expandVideo}
hideFooter
className={cn(
'bg-[#f8f9fa]/95! dark:bg-[#1c1c1c]/80!',
'border-[#e6e8eb]/90! dark:border-[#282828]/90!',
'transition ease-out',
'mx-auto backdrop-blur-md w-[calc(100%-2rem)]'
)}
onInteractOutside={(e) => {
// Only hide menu when clicking outside, not focusing outside
// Prevents Firefox dropdown issue that immediately closes menu after opening
if (e.type === 'dismissableLayer.pointerDownOutside') {
setExpandVideo(!expandVideo)
}
}}
size="xxlarge"
>
<div className="w-full! flex items-center justify-center">
<div className="relative w-full">
<button
onClick={() => setExpandVideo(false)}
className="text-foreground-light hover:text-foreground absolute -top-8 right-0"
>
<p className="text-xs">Close</p>
</button>
<div className="video-container rounded-lg! border-none! overflow-hidden!">
<iframe
src={`https://www.youtube-nocookie.com/embed/${videoId}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
<Dialog open={expandVideo} onOpenChange={(open) => setExpandVideo(open)}>
<DialogTrigger asChild>
<button
onClick={() => {
if (onOpenCallback) onOpenCallback()
setExpandVideo(true)
}}
className={['w-full', triggerContainerClassName].join(' ').trim()}
>
{trigger ?? <CliccablePreview />}
</button>
</DialogTrigger>
<DialogContent size="xxlarge">
<div className="w-full! flex items-center justify-center">
<div className="relative w-full">
<button
onClick={() => setExpandVideo(false)}
className="text-foreground-light hover:text-foreground absolute -top-8 right-0"
>
<p className="text-xs">Close</p>
</button>
<div className="video-container rounded-lg! border-none! overflow-hidden!">
<iframe
src={`https://www.youtube-nocookie.com/embed/${videoId}`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
</div>
</div>
</div>
</Modal>
<button
onClick={() => {
if (onOpenCallback) onOpenCallback()
setExpandVideo(true)
}}
className={['w-full', triggerContainerClassName].join(' ').trim()}
>
{trigger ?? <CliccablePreview />}
</button>
</DialogContent>
</Dialog>
</>
)
}
+16 -49
View File
@@ -1,6 +1,6 @@
import { useBreakpoint } from 'common'
import React, { ReactNode } from 'react'
import { cn, Modal } from 'ui'
import { cn, Dialog, DialogContent, DialogTrigger } from 'ui'
interface PopupFrameProps {
triggerContainerClassName?: string
@@ -20,48 +20,24 @@ export function PopupFrame({
const [open, setOpen] = React.useState(false)
const isMobile = useBreakpoint(768)
React.useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
switch (e.key) {
case 'Escape':
return setOpen(false)
default:
return
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [])
React.useEffect(() => {
if (isMobile) setOpen(false)
}, [isMobile])
return (
<>
<Modal
visible={open}
hideFooter
showCloseButton={false}
className={cn(
'bg-[#f8f9fa]/95! dark:bg-[#1c1c1c]/80!',
'border-[#e6e8eb]/90! dark:border-[#282828]/90!',
'transition ease-out',
'mx-auto backdrop-blur-md w-[calc(100%-2rem)]',
className
)}
onInteractOutside={(e) => {
// Only hide menu when clicking outside, not focusing outside
// Prevents Firefox dropdown issue that immediately closes menu after opening
if (e.type === 'dismissableLayer.pointerDownOutside') {
setOpen(!open)
}
}}
size="xxlarge"
>
<Dialog open={open} onOpenChange={(open) => setOpen(open)}>
<DialogTrigger asChild>
<button
onClick={() => {
if (onOpenCallback) onOpenCallback()
setOpen(true)
}}
className={cn('w-full', triggerContainerClassName)}
>
{trigger ?? 'Expand'}
</button>
</DialogTrigger>
<DialogContent className={className} size="xxlarge">
<div className="device-frame w-full! h-full flex items-center justify-center">
<div className="modal-group relative w-full h-full">
<button
@@ -75,16 +51,7 @@ export function PopupFrame({
</div>
</div>
</div>
</Modal>
<button
onClick={() => {
if (onOpenCallback) onOpenCallback()
setOpen(true)
}}
className={cn('w-full', triggerContainerClassName)}
>
{trigger ?? 'Expand'}
</button>
</>
</DialogContent>
</Dialog>
)
}
@@ -3,7 +3,19 @@
import { useConsentState } from 'common'
import Link from 'next/link'
import { PropsWithChildren, useState } from 'react'
import { Label, Modal, Switch } from 'ui'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
Label,
Switch,
} from 'ui'
import { Admonition } from '../admonition'
@@ -38,36 +50,21 @@ export const PrivacySettings = ({
setIsOpen(false)
}
const handleCancel = () => {
setIsOpen(false)
}
return (
<>
<button {...props} onClick={() => setIsOpen(true)}>
{children}
</button>
<Modal
closable
visible={isOpen}
alignFooter="right"
onCancel={handleCancel}
onConfirm={handleConfirmPreferences}
header="Privacy Settings"
onInteractOutside={(e) => {
// Only hide menu when clicking outside, not focusing outside
// Prevents Firefox dropdown issue that immediately closes menu after opening
if (e.type === 'dismissableLayer.pointerDownOutside') {
setIsOpen(!isOpen)
}
}}
className="max-w-[calc(100vw-4rem)]"
size="medium"
>
<div className="divide-y divide-border">
{categories === null ? (
<Modal.Content>
<Dialog open={isOpen} onOpenChange={(open) => setIsOpen(open)}>
<DialogTrigger asChild>
<button {...props} onClick={() => setIsOpen(true)}>
{children}
</button>
</DialogTrigger>
<DialogContent size="medium">
<DialogHeader>
<DialogTitle>Privacy Settings</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="space-y-4">
<div className="divide-y divide-border">
{categories === null ? (
<Admonition
type="warning"
title="Unable to Load Privacy Settings"
@@ -82,21 +79,25 @@ export const PrivacySettings = ({
</>
}
/>
</Modal.Content>
) : (
[...categories]
.reverse()
.map((category) => (
<Category
key={category.slug}
category={category}
handleServicesChange={handleServicesChange}
/>
))
)}
</div>
</Modal>
</>
) : (
[...categories]
.reverse()
.map((category) => (
<Category
key={category.slug}
category={category}
handleServicesChange={handleServicesChange}
/>
))
)}
</div>
</DialogSection>
<DialogFooter>
<Button onClick={() => setIsOpen(false)}>Cancel</Button>
<Button onClick={handleConfirmPreferences}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -134,32 +135,30 @@ function Category({
}
return (
<Modal.Content key={category.slug}>
<div className="flex flex-row items-center justify-between gap-4">
<div className="space-y-0.5">
<Label className="text-base" htmlFor={category.slug}>
{category.label}
</Label>
<div className="text-sm text-foreground-light" id={`${category.slug}-description`}>
{category.description}
<br />
<Link
href="https://supabase.com/privacy#8-cookies-and-similar-technologies-used-on-our-european-services"
className="underline"
>
Learn more
</Link>
</div>
<div className="flex flex-row items-center justify-between gap-4" key={category.slug}>
<div className="space-y-0.5">
<Label className="text-base" htmlFor={category.slug}>
{category.label}
</Label>
<div className="text-sm text-foreground-light" id={`${category.slug}-description`}>
{category.description}
<br />
<Link
href="https://supabase.com/privacy#8-cookies-and-similar-technologies-used-on-our-european-services"
className="underline"
>
Learn more
</Link>
</div>
<Switch
id={category.slug}
checked={isChecked}
disabled={category.isEssential}
defaultChecked={isChecked}
onCheckedChange={handleChange}
aria-describedby={`${category.slug}-description`}
/>
</div>
</Modal.Content>
<Switch
id={category.slug}
checked={isChecked}
disabled={category.isEssential}
defaultChecked={isChecked}
onCheckedChange={handleChange}
aria-describedby={`${category.slug}-description`}
/>
</div>
)
}