From 06c6a99e6d87a42e693ae4677bac3b82818ef33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Tue, 2 Jun 2026 08:25:50 +0300 Subject: [PATCH] Add functional QWord ribbon tabs --- src/features/qword/QWord.test.tsx | 88 ++++++++++++++++++++++++++++++- src/features/qword/QWord.tsx | 86 +++++++++++++++++++++++++++--- src/styles.css | 50 ++++++++++++++++++ 3 files changed, 216 insertions(+), 8 deletions(-) diff --git a/src/features/qword/QWord.test.tsx b/src/features/qword/QWord.test.tsx index be0f9fd..305bfa5 100644 --- a/src/features/qword/QWord.test.tsx +++ b/src/features/qword/QWord.test.tsx @@ -1,9 +1,13 @@ -import { fireEvent, render, waitFor } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; import { vi } from "vitest"; import type { WordDocument } from "../../types"; import { QWord, qwordCurrentPageFromViewport } from "./QWord"; +afterEach(() => { + cleanup(); +}); + describe("qwordCurrentPageFromViewport", () => { it("вычисляет текущую страницу по положению листа в области просмотра", () => { expect(qwordCurrentPageFromViewport({ stageTop: 34, paperTop: 227, pageHeight: 1123, pageCount: 3, zoom: 1 })).toBe(1); @@ -18,6 +22,86 @@ describe("qwordCurrentPageFromViewport", () => { }); describe("QWord", () => { + it("переключает вкладки ленты и показывает команды выбранной вкладки", () => { + const originalResizeObserver = globalThis.ResizeObserver; + const originalRequestAnimationFrame = window.requestAnimationFrame; + + globalThis.ResizeObserver = class { + observe() {} + disconnect() {} + } as unknown as typeof ResizeObserver; + window.requestAnimationFrame = ((callback: FrameRequestCallback) => { + callback(0); + return 1; + }) as typeof window.requestAnimationFrame; + + const document: WordDocument = { + id: "word-ribbon-tabs-test", + title: "Лента.docx", + updatedAt: "2026-06-02T00:00:00.000Z", + html: "

Текст

" + }; + + try { + render(); + + expect(screen.getByRole("tab", { name: "Главная" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByRole("region", { name: "Шрифт" })).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("tab", { name: "Вставка" })); + + expect(screen.getByRole("tab", { name: "Вставка" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByRole("region", { name: "Вставка" })).toBeInTheDocument(); + expect(screen.queryByRole("region", { name: "Шрифт" })).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole("tab", { name: "Файл" })); + + expect(screen.getByRole("tab", { name: "Файл" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByRole("region", { name: "Файл" })).toBeInTheDocument(); + expect(screen.queryByRole("region", { name: "Вставка" })).not.toBeInTheDocument(); + } finally { + globalThis.ResizeObserver = originalResizeObserver; + window.requestAnimationFrame = originalRequestAnimationFrame; + } + }); + + it("переключает режим просмотра документа на вкладке Вид", () => { + const originalResizeObserver = globalThis.ResizeObserver; + const originalRequestAnimationFrame = window.requestAnimationFrame; + + globalThis.ResizeObserver = class { + observe() {} + disconnect() {} + } as unknown as typeof ResizeObserver; + window.requestAnimationFrame = ((callback: FrameRequestCallback) => { + callback(0); + return 1; + }) as typeof window.requestAnimationFrame; + + const document: WordDocument = { + id: "word-view-mode-test", + title: "Вид.docx", + updatedAt: "2026-06-02T00:00:00.000Z", + html: "

Текст

" + }; + + try { + render(); + const stage = screen.getByLabelText("Редактор QWord"); + + expect(stage).toHaveClass("qword-view-print"); + + fireEvent.click(screen.getByRole("tab", { name: "Вид" })); + fireEvent.click(screen.getByRole("button", { name: "Веб-документ" })); + + expect(stage).toHaveClass("qword-view-web"); + expect(screen.getByRole("button", { name: "Веб-документ" })).toHaveAttribute("aria-pressed", "true"); + } finally { + globalThis.ResizeObserver = originalResizeObserver; + window.requestAnimationFrame = originalRequestAnimationFrame; + } + }); + it("обновляет текущую страницу в статусе при прокрутке документа", async () => { const originalResizeObserver = globalThis.ResizeObserver; const originalRequestAnimationFrame = window.requestAnimationFrame; diff --git a/src/features/qword/QWord.tsx b/src/features/qword/QWord.tsx index 55e0be2..507a225 100644 --- a/src/features/qword/QWord.tsx +++ b/src/features/qword/QWord.tsx @@ -143,7 +143,27 @@ const styleGalleryOptions = [ { value: "h6", label: "Заголовок 6" }, { value: "blockquote", label: "Цитата" } ]; -const wordRibbonTabs = ["Файл", "Главная", "Вставка", "Конструктор", "Макет", "Ссылки", "Рассылки", "Рецензирование", "Вид", "Справка"]; +type WordRibbonTabId = "file" | "home" | "insert" | "design" | "layout" | "references" | "mailings" | "review" | "view" | "help"; +type WordViewMode = "print" | "read" | "web"; + +const wordRibbonTabs: Array<{ id: WordRibbonTabId; label: string }> = [ + { id: "file", label: "Файл" }, + { id: "home", label: "Главная" }, + { id: "insert", label: "Вставка" }, + { id: "design", label: "Конструктор" }, + { id: "layout", label: "Макет" }, + { id: "references", label: "Ссылки" }, + { id: "mailings", label: "Рассылки" }, + { id: "review", label: "Рецензирование" }, + { id: "view", label: "Вид" }, + { id: "help", label: "Справка" } +]; + +const wordViewModes: Array<{ id: WordViewMode; label: string }> = [ + { id: "print", label: "Разметка" }, + { id: "read", label: "Чтение" }, + { id: "web", label: "Веб-документ" } +]; function findInPage(query: string) { return (window as PageSearchWindow).find?.(query) ?? false; @@ -169,9 +189,13 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP const editorRef = useRef(null); const fileInputRef = useRef(null); const imageInputRef = useRef(null); + const [activeRibbonTab, setActiveRibbonTab] = useState("home"); + const [viewMode, setViewMode] = useState("print"); const [pageView, setPageView] = useState({ count: 1, current: 1, height: defaultPageViewHeight }); const wordCount = useMemo(() => countWords(document.html), [document.html]); const stageStyle = { "--qword-zoom": String(zoom / 100) } as CSSProperties; + const activeRibbonTabLabel = wordRibbonTabs.find((tab) => tab.id === activeRibbonTab)?.label ?? "Главная"; + const stageClassName = ["document-stage", `qword-view-${viewMode}`].join(" "); function currentPageFromStage(pageCount: number, pageHeight: number) { const stage = stageRef.current; @@ -403,7 +427,7 @@ export function QWord({ document, zoom = 100, onChange, onStatusChange }: QWordP return (
-
+
- {wordRibbonTabs.map((tab, index) => ( - ))}
-
+
+ {activeRibbonTab === "file" ? (
Файл
+ ) : null} + {activeRibbonTab === "home" ? (
Буфер обмена
+ ) : null} + {activeRibbonTab === "home" || activeRibbonTab === "design" ? (