feat: Dialog UX Improvements - Scrollable Body & Click-Proof! 🎯
## ✅ 1 & 2: Dialog Improvements
### Scrollable Body with Fixed Header/Footer
**Problem:** Long content made header/footer disappear
**Solution:**
- Changed dialog to flexbox layout
- Added DialogBody component with overflow-y-auto
- Header and footer fixed with borders
- Max height 90vh
**Structure:**
```tsx
<DialogContent> (flex flex-col max-h-[90vh])
<DialogHeader> (px-6 pt-6 pb-4 border-b)
<DialogBody> (flex-1 overflow-y-auto px-6 py-4)
<DialogFooter> (px-6 py-4 border-t mt-auto)
</DialogContent>
```
### Close-Proof (No Outside Click)
**Problem:** Accidental outside clicks closed dialog
**Solution:**
- Added onPointerDownOutside preventDefault
- Added onInteractOutside preventDefault
- Must click X or Cancel to close
- No confusion or lost UI control
**Files:**
- `components/ui/dialog.tsx`
- `components/ui/rich-text-editor.tsx`
Ready for remaining improvements!
This commit is contained in:
@@ -35,14 +35,16 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-[99999] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
"fixed left-[50%] top-[50%] z-[99999] flex flex-col w-full max-w-lg max-h-[90vh] translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground z-10">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
@@ -57,7 +59,7 @@ const DialogHeader = ({
|
|||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
"flex flex-col space-y-1.5 text-center sm:text-left px-6 pt-6 pb-4 border-b",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -71,7 +73,7 @@ const DialogFooter = ({
|
|||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 px-6 py-4 border-t mt-auto",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -106,6 +108,20 @@ const DialogDescription = React.forwardRef<
|
|||||||
))
|
))
|
||||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const DialogBody = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 overflow-y-auto px-6 py-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogBody.displayName = "DialogBody"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
@@ -117,4 +133,5 @@ export {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogBody,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { Button } from './button';
|
|||||||
import { Input } from './input';
|
import { Input } from './input';
|
||||||
import { Label } from './label';
|
import { Label } from './label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogBody } from './dialog';
|
||||||
import { __ } from '@/lib/i18n';
|
import { __ } from '@/lib/i18n';
|
||||||
|
|
||||||
interface RichTextEditorProps {
|
interface RichTextEditorProps {
|
||||||
@@ -327,53 +327,55 @@ export function RichTextEditor({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<DialogBody>
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="btn-text">{__('Button Text')}</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="btn-text">{__('Button Text')}</Label>
|
||||||
id="btn-text"
|
<Input
|
||||||
value={buttonText}
|
id="btn-text"
|
||||||
onChange={(e) => setButtonText(e.target.value)}
|
value={buttonText}
|
||||||
placeholder={__('e.g., View Order')}
|
onChange={(e) => setButtonText(e.target.value)}
|
||||||
/>
|
placeholder={__('e.g., View Order')}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="btn-href">{__('Button Link')}</Label>
|
<Label htmlFor="btn-href">{__('Button Link')}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="btn-href"
|
id="btn-href"
|
||||||
value={buttonHref}
|
value={buttonHref}
|
||||||
onChange={(e) => setButtonHref(e.target.value)}
|
onChange={(e) => setButtonHref(e.target.value)}
|
||||||
placeholder="{order_url}"
|
placeholder="{order_url}"
|
||||||
/>
|
/>
|
||||||
{variables.length > 0 && (
|
{variables.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mt-2">
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
{variables.filter(v => v.includes('_url')).map((variable) => (
|
{variables.filter(v => v.includes('_url')).map((variable) => (
|
||||||
<code
|
<code
|
||||||
key={variable}
|
key={variable}
|
||||||
className="text-xs bg-muted px-2 py-1 rounded cursor-pointer hover:bg-muted/80"
|
className="text-xs bg-muted px-2 py-1 rounded cursor-pointer hover:bg-muted/80"
|
||||||
onClick={() => setButtonHref(buttonHref + `{${variable}}`)}
|
onClick={() => setButtonHref(buttonHref + `{${variable}}`)}
|
||||||
>
|
>
|
||||||
{`{${variable}}`}
|
{`{${variable}}`}
|
||||||
</code>
|
</code>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="btn-style">{__('Button Style')}</Label>
|
<Label htmlFor="btn-style">{__('Button Style')}</Label>
|
||||||
<Select value={buttonStyle} onValueChange={(value: 'solid' | 'outline') => setButtonStyle(value)}>
|
<Select value={buttonStyle} onValueChange={(value: 'solid' | 'outline') => setButtonStyle(value)}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="solid">{__('Solid (Primary)')}</SelectItem>
|
<SelectItem value="solid">{__('Solid (Primary)')}</SelectItem>
|
||||||
<SelectItem value="outline">{__('Outline (Secondary)')}</SelectItem>
|
<SelectItem value="outline">{__('Outline (Secondary)')}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DialogBody>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setButtonDialogOpen(false)}>
|
<Button variant="outline" onClick={() => setButtonDialogOpen(false)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user