Compare commits
3 Commits
docs/dev-s
...
feat/275-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba87f4ee24 | ||
|
|
5280392fc4 | ||
| 2524f39a36 |
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
|
||||||
|
// Covers the read-only render branch (PR #278): the language <Select> renders
|
||||||
|
// only when `editor.isEditable`; in read-only the copy button still shows.
|
||||||
|
// Mocks mirror the #146 structural harness (footnote-views.structure.test.tsx),
|
||||||
|
// except Select becomes a detectable node so we can assert its presence/absence.
|
||||||
|
vi.mock("@tiptap/react", () => ({
|
||||||
|
NodeViewWrapper: ({ children }: any) => <div>{children}</div>,
|
||||||
|
NodeViewContent: (props: any) => <div data-node-view-content="" {...props} />,
|
||||||
|
}));
|
||||||
|
vi.mock("react-i18next", () => ({
|
||||||
|
useTranslation: () => ({ t: (key: string) => key }),
|
||||||
|
}));
|
||||||
|
vi.mock("@mantine/core", () => ({
|
||||||
|
Group: ({ children }: any) => <div>{children}</div>,
|
||||||
|
Select: () => <div data-testid="language-select" />,
|
||||||
|
Tooltip: ({ children }: any) => <>{children}</>,
|
||||||
|
ActionIcon: ({ children, onClick }: any) => (
|
||||||
|
<button data-testid="copy-button" onClick={onClick}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
vi.mock("@/components/common/copy-button", () => ({
|
||||||
|
CopyButton: ({ children }: any) => children({ copied: false, copy: () => {} }),
|
||||||
|
}));
|
||||||
|
vi.mock("@tabler/icons-react", () => ({
|
||||||
|
IconCheck: () => null,
|
||||||
|
IconCopy: () => null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/features/editor/components/code-block/mermaid-view.tsx", () => ({
|
||||||
|
default: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import CodeBlockView from "./code-block-view";
|
||||||
|
|
||||||
|
const makeProps = (isEditable: boolean) =>
|
||||||
|
({
|
||||||
|
node: { attrs: { language: "javascript" }, textContent: "", nodeSize: 1 },
|
||||||
|
editor: {
|
||||||
|
state: { selection: { from: 0, to: 0 } },
|
||||||
|
isEditable,
|
||||||
|
commands: {},
|
||||||
|
on: vi.fn(),
|
||||||
|
off: vi.fn(),
|
||||||
|
},
|
||||||
|
extension: {
|
||||||
|
options: { lowlight: { listLanguages: () => ["javascript", "python"] } },
|
||||||
|
},
|
||||||
|
getPos: () => 0,
|
||||||
|
updateAttributes: () => {},
|
||||||
|
deleteNode: () => {},
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
describe("CodeBlockView language selector visibility (#278)", () => {
|
||||||
|
it("renders the language selector when the editor is editable", () => {
|
||||||
|
const { queryByTestId } = render(<CodeBlockView {...makeProps(true)} />);
|
||||||
|
expect(queryByTestId("language-select")).not.toBeNull();
|
||||||
|
expect(queryByTestId("copy-button")).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the language selector in read-only but keeps the copy button", () => {
|
||||||
|
const { queryByTestId } = render(<CodeBlockView {...makeProps(false)} />);
|
||||||
|
expect(queryByTestId("language-select")).toBeNull();
|
||||||
|
expect(queryByTestId("copy-button")).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -50,10 +50,10 @@ export default function CodeBlockView(props: NodeViewProps) {
|
|||||||
{/* #146: the editable <pre><code> (contentDOM) MUST come first in the DOM.
|
{/* #146: the editable <pre><code> (contentDOM) MUST come first in the DOM.
|
||||||
With the non-editable menu rendered before it, the browser's click
|
With the non-editable menu rendered before it, the browser's click
|
||||||
hit-testing snapped the caret up one line. Render content first; the
|
hit-testing snapped the caret up one line. Render content first; the
|
||||||
menu is rendered after it and lifted back above visually via flex
|
menu is rendered after it and floated into the top-right corner as an
|
||||||
`order: -1` (the `.codeBlock` wrapper is a flex column — see
|
absolute overlay (see `.menuGroup` in code-block.module.css, anchored
|
||||||
code-block.module.css). It stays fully in flow as a full-width row
|
to the `position: relative` `.codeBlock` wrapper in code.css). It no
|
||||||
above the code: no overlay/absolute positioning. The second #146
|
longer takes a full-width row above the code. The second #146
|
||||||
mitigation lives in editor-paste-handler.tsx (reflowAfterPaste). */}
|
mitigation lives in editor-paste-handler.tsx (reflowAfterPaste). */}
|
||||||
<pre
|
<pre
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
@@ -67,11 +67,12 @@ export default function CodeBlockView(props: NodeViewProps) {
|
|||||||
<NodeViewContent as="code" className={`language-${language}`} />
|
<NodeViewContent as="code" className={`language-${language}`} />
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<Group
|
<Group contentEditable={false} className={classes.menuGroup}>
|
||||||
justify="flex-end"
|
{/* In read-only (published) there is no language selector at all —
|
||||||
contentEditable={false}
|
only the copy button. When editable the selector is hidden until
|
||||||
className={classes.menuGroup}
|
the block is hovered/focused (or its dropdown is open) via the
|
||||||
>
|
`.languageSelect` class (see code-block.module.css). */}
|
||||||
|
{editor.isEditable && (
|
||||||
<Select
|
<Select
|
||||||
placeholder="auto"
|
placeholder="auto"
|
||||||
checkIconPosition="right"
|
checkIconPosition="right"
|
||||||
@@ -80,9 +81,9 @@ export default function CodeBlockView(props: NodeViewProps) {
|
|||||||
onChange={changeLanguage}
|
onChange={changeLanguage}
|
||||||
searchable
|
searchable
|
||||||
style={{ maxWidth: "130px" }}
|
style={{ maxWidth: "130px" }}
|
||||||
classNames={{ input: classes.selectInput }}
|
classNames={{ root: classes.languageSelect, input: classes.selectInput }}
|
||||||
disabled={!editor.isEditable}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<CopyButton value={node?.textContent} timeout={2000}>
|
<CopyButton value={node?.textContent} timeout={2000}>
|
||||||
{({ copied, copy }) => (
|
{({ copied, copy }) => (
|
||||||
|
|||||||
@@ -17,15 +17,37 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #146: the menu now follows the <pre> in the DOM (so the editable contentDOM is
|
/* #146: the menu follows the <pre> in the DOM (so the editable contentDOM is
|
||||||
FIRST and click hit-testing is correct). Lift it back ABOVE the code visually
|
FIRST and click hit-testing is correct). Instead of sitting in-flow, it is
|
||||||
with flex `order` — the .codeBlock wrapper is a flex column (see code.css) —
|
floated into the top-right corner as an absolute overlay anchored to the
|
||||||
so the menu still reads as a row above the code, exactly as before, without
|
`position: relative` .codeBlock wrapper (see code.css), so it no longer
|
||||||
sitting in-flow before the contentDOM. */
|
takes a full-width row above the code. The Mantine dropdown is portaled, so
|
||||||
|
it is never clipped by the overlay. */
|
||||||
.menuGroup {
|
.menuGroup {
|
||||||
order: -1;
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The language selector is hidden until the block is hovered, or the selector
|
||||||
|
itself is focused / its dropdown is open. It keeps its width in the flex
|
||||||
|
Group (only opacity toggles) so the copy button never jumps, and
|
||||||
|
`pointer-events: none` while hidden lets clicks fall through to the code.
|
||||||
|
`.codeBlock` is the global NodeViewWrapper class → use :global(). */
|
||||||
|
.languageSelect {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.codeBlock):hover .languageSelect,
|
||||||
|
.languageSelect:focus-within {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
.codeBlock {
|
.codeBlock {
|
||||||
/* #146: flex column so the menu (rendered AFTER <pre> in the DOM, so the
|
/* #146: flex column keeps the editable <pre> (first in the DOM so click
|
||||||
editable contentDOM is first) is lifted back above the code via `order`. */
|
hit-testing is correct) laid out above any Mermaid diagram. `position:
|
||||||
|
relative` anchors the control panel, which is floated into the top-right
|
||||||
|
corner as an absolute overlay (see `.menuGroup` in code-block.module.css). */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: var(--mantine-radius-default);
|
border-radius: var(--mantine-radius-default);
|
||||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||||
|
|||||||
Reference in New Issue
Block a user