diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 62927f66a..c0b67e9d1 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -277,6 +277,9 @@ "Align left": "Align left", "Align right": "Align right", "Align center": "Align center", + "Alt text": "Alt text", + "Describe this for accessibility.": "Describe this for accessibility.", + "Add a description": "Add a description", "Justify": "Justify", "Merge cells": "Merge cells", "Split cell": "Split cell", diff --git a/apps/client/src/features/editor/components/common/use-alt-text-control.tsx b/apps/client/src/features/editor/components/common/use-alt-text-control.tsx new file mode 100644 index 000000000..1a43f9d79 --- /dev/null +++ b/apps/client/src/features/editor/components/common/use-alt-text-control.tsx @@ -0,0 +1,139 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Editor } from "@tiptap/react"; +import { + ActionIcon, + Button, + Group, + Paper, + Text, + Textarea, + Tooltip, +} from "@mantine/core"; +import { IconAlt } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; + +const ALT_MAX_LENGTH = 300; + +function sanitizeAlt(value: string): string { + return value + .replace(/[\\\[\]!]/g, "") + .replace(/\s+/g, " ") + .trim(); +} + +type UseAltTextControlArgs = { + editor: Editor; + nodeName: string; + currentAlt: string; +}; + +export function useAltTextControl({ + editor, + nodeName, + currentAlt, +}: UseAltTextControlArgs) { + const { t } = useTranslation(); + const [showInput, setShowInput] = useState(false); + const [draft, setDraft] = useState(""); + + const open = useCallback(() => { + setDraft(currentAlt || ""); + setShowInput(true); + }, [currentAlt]); + + useEffect(() => { + const handler = () => { + if (!editor.isActive(nodeName)) { + setShowInput(false); + } + }; + editor.on("selectionUpdate", handler); + return () => { + editor.off("selectionUpdate", handler); + }; + }, [editor, nodeName]); + + const cancel = useCallback(() => { + setShowInput(false); + }, []); + + const save = useCallback(() => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .updateAttributes(nodeName, { alt: sanitizeAlt(draft) || undefined }) + .run(); + setShowInput(false); + }, [editor, nodeName, draft]); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + save(); + } else if (e.key === "Escape") { + e.preventDefault(); + cancel(); + } + }, + [save, cancel], + ); + + const button = ( + + + + + + ); + + const panel = showInput ? ( + + + {t("Alt text")} + + + {t("Describe this for accessibility.")} + +