Wire quick access commands

This commit is contained in:
Курнат Андрей
2026-06-02 20:39:30 +03:00
parent 231bb6c076
commit c16df37c95
5 changed files with 76 additions and 10 deletions
+31 -2
View File
@@ -1,8 +1,12 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { AppShell } from "./AppShell";
describe("AppShell", () => {
afterEach(() => {
cleanup();
});
it("не показывает общую ленту навигации между приложениями", () => {
const view = render(
<AppShell activeApp="word" status={{ savedLabel: "Сохранено локально" }}>
@@ -37,4 +41,29 @@ describe("AppShell", () => {
expect(screen.getByText("Число слов: 42")).toBeInTheDocument();
expect(screen.getByText("125%")).toBeInTheDocument();
});
it("отправляет команды панели быстрого доступа активному приложению", () => {
const commands: string[] = [];
function onCommand(event: WindowEventMap["qoffice-command"]) {
commands.push(event.detail.command);
}
window.addEventListener("qoffice-command", onCommand);
try {
const view = render(
<AppShell activeApp="word" status={{ savedLabel: "Сохранено локально" }}>
<div>Рабочая область QWord</div>
</AppShell>
);
const quickAccessButtons = view.container.querySelectorAll(".quick-access button");
fireEvent.click(quickAccessButtons[0]);
fireEvent.click(quickAccessButtons[1]);
fireEvent.click(quickAccessButtons[2]);
expect(commands).toEqual(["save", "undo", "redo"]);
} finally {
window.removeEventListener("qoffice-command", onCommand);
}
});
});
+13 -4
View File
@@ -6,7 +6,8 @@ import {
Share2,
Undo2
} from "lucide-react";
import type { ReactNode } from "react";
import type { MouseEvent, ReactNode } from "react";
import type { QOfficeCommand } from "../hooks/useQOfficeCommand";
import type { AppId } from "../types";
interface AppShellProps {
@@ -28,6 +29,14 @@ const appTitles: Record<AppId, string> = {
mail: "Почта - QOutlook"
};
function dispatchQOfficeCommand(command: QOfficeCommand) {
window.dispatchEvent(new CustomEvent("qoffice-command", { detail: { command } }));
}
function keepActiveEditorFocus(event: MouseEvent<HTMLButtonElement>) {
event.preventDefault();
}
export function AppShell({ activeApp, status, children }: AppShellProps) {
const statusClassName = ["statusbar", status.className].filter(Boolean).join(" ");
@@ -36,13 +45,13 @@ export function AppShell({ activeApp, status, children }: AppShellProps) {
<main className="workspace">
<header className="titlebar">
<div className="quick-access" aria-label="Панель быстрого доступа">
<button type="button" title="Сохранить" aria-label="Сохранить">
<button type="button" title="Сохранить" aria-label="Сохранить" onMouseDown={keepActiveEditorFocus} onClick={() => dispatchQOfficeCommand("save")}>
<Save size={16} />
</button>
<button type="button" title="Отменить" aria-label="Отменить">
<button type="button" title="Отменить" aria-label="Отменить" onMouseDown={keepActiveEditorFocus} onClick={() => dispatchQOfficeCommand("undo")}>
<Undo2 size={16} />
</button>
<button type="button" title="Повторить" aria-label="Повторить">
<button type="button" title="Повторить" aria-label="Повторить" onMouseDown={keepActiveEditorFocus} onClick={() => dispatchQOfficeCommand("redo")}>
<Redo2 size={16} />
</button>
<button type="button" title="Настроить панель" aria-label="Настроить панель">
+1 -1
View File
@@ -1,7 +1,7 @@
export {};
type DesktopOfficeFileKind = "docx" | "xlsx" | "pptx" | "eml" | "mail";
type QOfficeDesktopCommand = "open" | "save" | "export-pdf" | "print";
type QOfficeDesktopCommand = "open" | "save" | "export-pdf" | "print" | "undo" | "redo";
interface DesktopOpenOfficeFileResult {
canceled: boolean;
+25 -2
View File
@@ -1,5 +1,5 @@
import { render, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { cleanup, render, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useQOfficeCommand } from "./useQOfficeCommand";
function CommandProbe({ onSave, onExportPdf }: { onSave: () => void; onExportPdf?: () => void }) {
@@ -8,6 +8,10 @@ function CommandProbe({ onSave, onExportPdf }: { onSave: () => void; onExportPdf
}
describe("useQOfficeCommand", () => {
afterEach(() => {
cleanup();
});
it("вызывает обработчик команды QOffice", async () => {
const onSave = vi.fn();
render(<CommandProbe onSave={onSave} />);
@@ -39,4 +43,23 @@ describe("useQOfficeCommand", () => {
await waitFor(() => expect(onExportPdf).toHaveBeenCalledTimes(1));
expect(onSave).not.toHaveBeenCalled();
});
it("использует команду документа для undo и redo без обработчика приложения", async () => {
const originalExecCommand = document.execCommand;
const execCommand = vi.fn();
Object.defineProperty(document, "execCommand", { configurable: true, value: execCommand });
try {
render(<CommandProbe onSave={vi.fn()} />);
window.dispatchEvent(new CustomEvent("qoffice-command", { detail: { command: "undo" } }));
window.dispatchEvent(new CustomEvent("qoffice-command", { detail: { command: "redo" } }));
await waitFor(() => expect(execCommand).toHaveBeenCalledWith("redo"));
expect(execCommand).toHaveBeenNthCalledWith(1, "undo");
expect(execCommand).toHaveBeenNthCalledWith(2, "redo");
} finally {
Object.defineProperty(document, "execCommand", { configurable: true, value: originalExecCommand });
}
});
});
+6 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef } from "react";
export type QOfficeCommand = "open" | "save" | "export-pdf" | "print";
export type QOfficeCommand = "open" | "save" | "export-pdf" | "print" | "undo" | "redo";
export type QOfficeCommandHandlers = Partial<Record<QOfficeCommand, () => void | Promise<void>>>;
export function useQOfficeCommand(handlers: QOfficeCommandHandlers) {
@@ -16,6 +16,11 @@ export function useQOfficeCommand(handlers: QOfficeCommandHandlers) {
const handler = handlersRef.current[command];
if (handler) {
void handler();
return;
}
if (command === "undo" || command === "redo") {
globalThis.document.execCommand?.(command);
}
}