Feature: Implement full-screen mode for rich text editor (#2658)

This commit is contained in:
Amruth Pillai
2026-01-31 02:29:34 +01:00
committed by GitHub
parent 97274d1c80
commit a8d8d0e340
55 changed files with 504 additions and 74 deletions
+87 -24
View File
@@ -2,6 +2,8 @@ import { t } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
import { Trans } from "@lingui/react/macro";
import {
ArrowsInSimpleIcon,
ArrowsOutSimpleIcon,
CodeBlockIcon,
CodeSimpleIcon,
ColumnsPlusLeftIcon,
@@ -48,11 +50,13 @@ import {
useEditorState,
} from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { useMemo } from "react";
import { VisuallyHidden } from "radix-ui";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { match } from "ts-pattern";
import z from "zod";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -103,6 +107,7 @@ type Props = UseEditorOptions & {
export function RichInput({ value, onChange, style, className, editorClassName, ...options }: Props) {
const { i18n } = useLingui();
const textDirection = isRTL(i18n.locale) ? "rtl" : undefined;
const [isFullscreen, setIsFullscreen] = useState(false);
const editor = useEditor({
...options,
@@ -115,11 +120,13 @@ export function RichInput({ value, onChange, style, className, editorClassName,
attributes: {
spellcheck: "false",
"data-editor": "true",
"data-fullscreen": isFullscreen ? "true" : "false",
class: cn(
"group/editor",
"max-h-[400px] min-h-[100px] overflow-y-auto p-3 pb-0",
"group/editor overflow-y-auto p-3 pb-4",
"rounded-md rounded-t-none border outline-none focus-visible:border-ring",
"[td:has(.selectedCell)]:bg-primary",
"data-[fullscreen=false]:max-h-[400px] data-[fullscreen=false]:min-h-[100px]",
"data-[fullscreen=true]:max-h-none data-[fullscreen=true]:min-h-full",
styles.tiptap_content,
styles.editor_content,
editorClassName,
@@ -135,17 +142,55 @@ export function RichInput({ value, onChange, style, className, editorClassName,
if (!editor) return null;
const editorElement = (
<div className="relative">
<EditorToolbar editor={editor} isFullscreen={isFullscreen} />
<EditorContent editor={editor} />
<Button
size="icon"
variant="secondary"
className="absolute right-2 bottom-2 size-7"
title={isFullscreen ? t`Exit Fullscreen` : t`Fullscreen`}
onClick={() => setIsFullscreen(!isFullscreen)}
>
{isFullscreen ? <ArrowsInSimpleIcon className="size-4" /> : <ArrowsOutSimpleIcon className="size-4" />}
</Button>
</div>
);
if (isFullscreen) {
return (
<EditorContext value={providerValue}>
<div className={cn("rounded-md", className)} style={style}>
{/* Placeholder to maintain layout */}
<div className="h-[200px] rounded-md border border-dashed" />
</div>
<Dialog open={isFullscreen} onOpenChange={setIsFullscreen}>
<DialogContent className="flex h-[95svh] max-h-none! w-[95svw] max-w-none! flex-col p-4 sm:max-w-none! 2xl:max-w-none!">
<VisuallyHidden.Root>
<DialogTitle>Fullscreen Editor</DialogTitle>
<DialogDescription>Edit content in fullscreen mode</DialogDescription>
</VisuallyHidden.Root>
{editorElement}
</DialogContent>
</Dialog>
</EditorContext>
);
}
return (
<EditorContext value={providerValue}>
<div className={cn("rounded-md", className)} style={style}>
<EditorToolbar editor={editor} />
<EditorContent editor={editor} />
{editorElement}
</div>
</EditorContext>
);
}
function EditorToolbar({ editor }: { editor: Editor }) {
function EditorToolbar({ editor, isFullscreen }: { editor: Editor; isFullscreen: boolean }) {
const prompt = usePrompt();
const state = useEditorState({
@@ -313,7 +358,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
return (
<div className="flex flex-wrap items-center gap-y-0.5 rounded-md rounded-b-none border border-b-0">
<Toggle
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
className="rounded-none"
title={t`Bold`}
@@ -325,7 +370,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</Toggle>
<Toggle
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
className="rounded-none"
title={t`Italic`}
@@ -337,7 +382,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</Toggle>
<Toggle
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
className="rounded-none"
title={t`Underline`}
@@ -349,7 +394,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</Toggle>
<Toggle
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
className="rounded-none"
title={t`Strike`}
@@ -361,7 +406,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</Toggle>
<Toggle
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
className="rounded-none"
title={t`Highlight`}
@@ -376,7 +421,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" tabIndex={-1} variant="ghost" className="rounded-none">
<Button size={isFullscreen ? "lg" : "sm"} tabIndex={-1} variant="ghost" className="rounded-none">
{match(state)
.with({ isParagraph: true }, () => <ParagraphIcon className="size-3.5" />)
.with({ isHeading1: true }, () => <TextHOneIcon className="size-3.5" />)
@@ -446,7 +491,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" tabIndex={-1} variant="ghost" className="rounded-none">
<Button size={isFullscreen ? "lg" : "sm"} tabIndex={-1} variant="ghost" className="rounded-none">
{match(state)
.with({ isLeftAlign: true }, () => <TextAlignLeftIcon className="size-3.5" />)
.with({ isCenterAlign: true }, () => <TextAlignCenterIcon className="size-3.5" />)
@@ -492,7 +537,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
<div className="mx-1 h-5 w-px bg-border" />
<Toggle
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
className="rounded-none"
title={t`Bullet List`}
@@ -504,7 +549,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</Toggle>
<Toggle
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
className="rounded-none"
title={t`Ordered List`}
@@ -516,7 +561,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</Toggle>
<Button
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
variant="ghost"
className="rounded-none"
@@ -527,7 +572,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</Button>
<Button
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
variant="ghost"
className="rounded-none"
@@ -540,17 +585,29 @@ function EditorToolbar({ editor }: { editor: Editor }) {
<div className="mx-1 h-5 w-px bg-border" />
{state.isLink ? (
<Button size="sm" tabIndex={-1} variant="ghost" className="rounded-none" onClick={state.unsetLink}>
<Button
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
variant="ghost"
className="rounded-none"
onClick={state.unsetLink}
>
<LinkBreakIcon className="size-3.5" />
</Button>
) : (
<Button size="sm" tabIndex={-1} variant="ghost" className="rounded-none" onClick={state.setLink}>
<Button
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
variant="ghost"
className="rounded-none"
onClick={state.setLink}
>
<LinkIcon className="size-3.5" />
</Button>
)}
<Toggle
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
className="rounded-none"
title={t`Inline Code`}
@@ -562,7 +619,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</Toggle>
<Toggle
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
className="rounded-none"
title={t`Code Block`}
@@ -575,7 +632,13 @@ function EditorToolbar({ editor }: { editor: Editor }) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" tabIndex={-1} variant="ghost" className="rounded-none" title={t`Table`}>
<Button
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
variant="ghost"
className="rounded-none"
title={t`Table`}
>
<TableIcon className="size-3.5" />
</Button>
</DropdownMenuTrigger>
@@ -621,7 +684,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</DropdownMenu>
<Button
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
variant="ghost"
className="rounded-none"
@@ -632,7 +695,7 @@ function EditorToolbar({ editor }: { editor: Editor }) {
</Button>
<Button
size="sm"
size={isFullscreen ? "lg" : "sm"}
tabIndex={-1}
variant="ghost"
className="rounded-none"