Skip to content

Commit 59453d5

Browse files
committed
refactor(editor): simplify toolbar interactions
1 parent 9a4c805 commit 59453d5

3 files changed

Lines changed: 37 additions & 77 deletions

File tree

apps/frontend-manage/src/components/common/ContentInput.tsx

Lines changed: 30 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,16 @@ import { common, createLowlight } from 'lowlight'
2424
import { useTranslations } from 'next-intl'
2525
import React, { useEffect, useState } from 'react'
2626
import { twMerge } from 'tailwind-merge'
27-
import MediaLibrary from '~/components/common/MediaLibrary'
27+
import MediaLibrary from './MediaLibrary'
2828

2929
const lowlight = createLowlight(common)
3030

3131
const ToolbarContext = React.createContext<{ disabled: boolean }>({
3232
disabled: false,
3333
})
3434

35-
const normalizeMarkdown = (str: string) => {
36-
return str.replace(/\r\n/g, '\n').replace(/\n+$/, '')
37-
}
35+
const normalizeMarkdown = (str: string) =>
36+
str.replace(/\r\n/g, '\n').replace(/\n+$/, '')
3837

3938
export interface ContentInputClassName {
4039
root?: string
@@ -51,7 +50,7 @@ interface Props {
5150
showToolbarOnFocus?: boolean
5251
placeholder: string
5352
autoFocus?: boolean
54-
content: string
53+
content?: string
5554
className?: ContentInputClassName
5655
data?: {
5756
test?: string
@@ -82,15 +81,15 @@ function ContentInput({
8281
Image,
8382
Markdown,
8483
Placeholder.configure({
85-
placeholder: placeholder,
84+
placeholder,
8685
emptyEditorClass: 'is-editor-empty',
8786
}),
8887
CodeBlockLowlight.configure({
8988
lowlight,
9089
}),
9190
TableKit,
9291
],
93-
content: content,
92+
content: content ?? '',
9493
contentType: 'markdown',
9594
autofocus: autoFocus ? 'end' : false,
9695
editable: !disabled,
@@ -168,7 +167,7 @@ function ContentInput({
168167
showToolbarOnFocus && 'hidden group-focus-within:flex'
169168
)}
170169
>
171-
<ToolbarContext.Provider value={{ disabled: !!disabled }}>
170+
<ToolbarContext.Provider value={{ disabled }}>
172171
<div
173172
className={twMerge(
174173
'flex flex-1 flex-row gap-1',
@@ -179,10 +178,7 @@ function ContentInput({
179178
title={t('shared.contentInput.boldStyle')}
180179
aria-label={t('shared.contentInput.boldStyle')}
181180
active={editor.isActive('bold')}
182-
onClick={(e: React.MouseEvent) => {
183-
e.preventDefault()
184-
editor.chain().focus().toggleBold().run()
185-
}}
181+
onClick={() => editor.chain().focus().toggleBold().run()}
186182
>
187183
<FontAwesomeIcon
188184
icon={faBold}
@@ -194,10 +190,7 @@ function ContentInput({
194190
title={t('shared.contentInput.italicStyle')}
195191
aria-label={t('shared.contentInput.italicStyle')}
196192
active={editor.isActive('italic')}
197-
onClick={(e: React.MouseEvent) => {
198-
e.preventDefault()
199-
editor.chain().focus().toggleItalic().run()
200-
}}
193+
onClick={() => editor.chain().focus().toggleItalic().run()}
201194
>
202195
<FontAwesomeIcon
203196
icon={faItalic}
@@ -209,10 +202,7 @@ function ContentInput({
209202
title={t('shared.contentInput.codeStyle')}
210203
aria-label={t('shared.contentInput.codeStyle')}
211204
active={editor.isActive('code')}
212-
onClick={(e: React.MouseEvent) => {
213-
e.preventDefault()
214-
editor.chain().focus().toggleCode().run()
215-
}}
205+
onClick={() => editor.chain().focus().toggleCode().run()}
216206
>
217207
<FontAwesomeIcon
218208
icon={faCode}
@@ -224,10 +214,7 @@ function ContentInput({
224214
title={t('shared.contentInput.citationStyle')}
225215
aria-label={t('shared.contentInput.citationStyle')}
226216
active={editor.isActive('blockquote')}
227-
onClick={(e: React.MouseEvent) => {
228-
e.preventDefault()
229-
editor.chain().focus().toggleBlockquote().run()
230-
}}
217+
onClick={() => editor.chain().focus().toggleBlockquote().run()}
231218
>
232219
<FontAwesomeIcon
233220
icon={faQuoteRight}
@@ -239,10 +226,7 @@ function ContentInput({
239226
title={t('shared.contentInput.numberedList')}
240227
aria-label={t('shared.contentInput.numberedList')}
241228
active={editor.isActive('orderedList')}
242-
onClick={(e: React.MouseEvent) => {
243-
e.preventDefault()
244-
editor.chain().focus().toggleOrderedList().run()
245-
}}
229+
onClick={() => editor.chain().focus().toggleOrderedList().run()}
246230
>
247231
<FontAwesomeIcon
248232
icon={faListOl}
@@ -254,10 +238,7 @@ function ContentInput({
254238
title={t('shared.contentInput.unnumberedList')}
255239
aria-label={t('shared.contentInput.unnumberedList')}
256240
active={editor.isActive('bulletList')}
257-
onClick={(e: React.MouseEvent) => {
258-
e.preventDefault()
259-
editor.chain().focus().toggleBulletList().run()
260-
}}
241+
onClick={() => editor.chain().focus().toggleBulletList().run()}
261242
>
262243
<FontAwesomeIcon
263244
icon={faListUl}
@@ -269,19 +250,15 @@ function ContentInput({
269250
title={t('shared.contentInput.image')}
270251
aria-label={t('shared.contentInput.image')}
271252
active={isImageDropzoneOpen}
272-
onClick={(e: React.MouseEvent) => {
273-
e.preventDefault()
274-
setIsImageDropzoneOpen((prev) => !prev)
275-
}}
253+
onClick={() => setIsImageDropzoneOpen((prev) => !prev)}
276254
>
277255
<FontAwesomeIcon icon={faImage} color="grey" />
278256
</ToolbarButton>
279257

280258
<ToolbarButton
281259
title={t('shared.contentInput.latex')}
282260
aria-label={t('shared.contentInput.latex')}
283-
onClick={(e: React.MouseEvent) => {
284-
e.preventDefault()
261+
onClick={() => {
285262
editor
286263
.chain()
287264
.focus()
@@ -295,8 +272,7 @@ function ContentInput({
295272
<ToolbarButton
296273
title={t('shared.contentInput.latexCentered')}
297274
aria-label={t('shared.contentInput.latexCentered')}
298-
onClick={(e: React.MouseEvent) => {
299-
e.preventDefault()
275+
onClick={() => {
300276
editor
301277
.chain()
302278
.focus()
@@ -313,13 +289,11 @@ function ContentInput({
313289
</ToolbarButton>
314290

315291
<ToolbarButton
292+
data-cy="toolbar-code-block"
316293
title={t('shared.contentInput.codeBlock')}
317294
aria-label={t('shared.contentInput.codeBlock')}
318295
active={editor.isActive('codeBlock')}
319-
onClick={(e: React.MouseEvent) => {
320-
e.preventDefault()
321-
editor.chain().focus().toggleCodeBlock().run()
322-
}}
296+
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
323297
>
324298
<FontAwesomeIcon
325299
icon={faTerminal}
@@ -332,8 +306,7 @@ function ContentInput({
332306
title={t('shared.contentInput.table')}
333307
aria-label={t('shared.contentInput.table')}
334308
active={editor.isActive('table')}
335-
onClick={(e: React.MouseEvent) => {
336-
e.preventDefault()
309+
onClick={() => {
337310
if (!editor.isActive('table')) {
338311
editor
339312
.chain()
@@ -356,50 +329,39 @@ function ContentInput({
356329
data-cy="table-add-row"
357330
title={t('shared.contentInput.addRow')}
358331
aria-label={t('shared.contentInput.addRow')}
359-
onClick={(e: React.MouseEvent) => {
360-
e.preventDefault()
361-
editor.chain().focus().addRowAfter().run()
362-
}}
332+
onClick={() => editor.chain().focus().addRowAfter().run()}
363333
>
364334
<span className="text-[10px] font-bold">+R</span>
365335
</ToolbarButton>
366336
<ToolbarButton
337+
data-cy="table-delete-row"
367338
title={t('shared.contentInput.deleteRow')}
368339
aria-label={t('shared.contentInput.deleteRow')}
369-
onClick={(e: React.MouseEvent) => {
370-
e.preventDefault()
371-
editor.chain().focus().deleteRow().run()
372-
}}
340+
onClick={() => editor.chain().focus().deleteRow().run()}
373341
>
374342
<span className="text-[10px] font-bold text-red-500">-R</span>
375343
</ToolbarButton>
376344
<ToolbarButton
345+
data-cy="table-add-column"
377346
title={t('shared.contentInput.addColumn')}
378347
aria-label={t('shared.contentInput.addColumn')}
379-
onClick={(e: React.MouseEvent) => {
380-
e.preventDefault()
381-
editor.chain().focus().addColumnAfter().run()
382-
}}
348+
onClick={() => editor.chain().focus().addColumnAfter().run()}
383349
>
384350
<span className="text-[10px] font-bold">+C</span>
385351
</ToolbarButton>
386352
<ToolbarButton
353+
data-cy="table-delete-column"
387354
title={t('shared.contentInput.deleteColumn')}
388355
aria-label={t('shared.contentInput.deleteColumn')}
389-
onClick={(e: React.MouseEvent) => {
390-
e.preventDefault()
391-
editor.chain().focus().deleteColumn().run()
392-
}}
356+
onClick={() => editor.chain().focus().deleteColumn().run()}
393357
>
394358
<span className="text-[10px] font-bold text-red-500">-C</span>
395359
</ToolbarButton>
396360
<ToolbarButton
361+
data-cy="table-delete"
397362
title={t('shared.contentInput.deleteTable')}
398363
aria-label={t('shared.contentInput.deleteTable')}
399-
onClick={(e: React.MouseEvent) => {
400-
e.preventDefault()
401-
editor.chain().focus().deleteTable().run()
402-
}}
364+
onClick={() => editor.chain().focus().deleteTable().run()}
403365
>
404366
<span className="text-[10px] font-bold text-red-700">Del</span>
405367
</ToolbarButton>
@@ -409,10 +371,7 @@ function ContentInput({
409371
<ToolbarButton
410372
title={t('shared.contentInput.undo')}
411373
aria-label={t('shared.contentInput.undo')}
412-
onClick={(e: React.MouseEvent) => {
413-
e.preventDefault()
414-
editor.chain().focus().undo().run()
415-
}}
374+
onClick={() => editor.chain().focus().undo().run()}
416375
className="mr-3"
417376
>
418377
<FontAwesomeIcon icon={faRotateLeft} color="grey" />
@@ -421,10 +380,7 @@ function ContentInput({
421380
<ToolbarButton
422381
title={t('shared.contentInput.redo')}
423382
aria-label={t('shared.contentInput.redo')}
424-
onClick={(e: React.MouseEvent) => {
425-
e.preventDefault()
426-
editor.chain().focus().redo().run()
427-
}}
383+
onClick={() => editor.chain().focus().redo().run()}
428384
className="mr-0.5"
429385
>
430386
<FontAwesomeIcon icon={faRotateRight} color="grey" />

playwright/tests/ZA-editor-rich-features.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ test.describe('Test Tiptap Editor Rich Text, Table, Code Block, and Preview Feat
104104
await expect(tableElement.locator('tr')).toHaveCount(4)
105105

106106
// Add a column and keep it through the Markdown save/preview round-trip
107-
await page.getByRole('button', { name: 'Add column' }).click()
107+
await page.getByTestId('table-add-column').click()
108108
await expect(
109109
tableElement.locator('tr').first().locator('th, td')
110110
).toHaveCount(4)

project/2026-07-10-pr-5148-tiptap-editor-finalization-plan.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,12 @@ Outcome: PR ready for human approval; no hidden process debt.
436436
- `2026-07-10 | Slice 4 | DONE_WITH_CONCERNS | Browser/devrouter proved the previous merge control was unsafe: a live two-column merge produced colspan=2, while recovered serialized state returned nine ordinary cells with colspan=1. Agy implemented the bounded product patch: bare TableKit, Lowlight common, native labelled toolbar buttons, generic manage preview code styling, and paired merge-key removal. Follow-up review fixed command-button aria-pressed semantics, removed dead resize CSS, and expanded ZA to reject merge/resize UI, persist a fourth row and column, and assert JavaScript, TypeScript, and R tokens in the editor and shared StudentElement preview. Browser after the patch rendered a 3x3 table, added a fourth row, exposed no merge or resize controls, contained zero nested buttons, and showed a visible keyboard focus ring at desktop and narrow viewports. Manage /questions/[id] is the same shared StudentElement renderer used by PWA flows. Prettier, frontend-manage tsc, Playwright tsc, scoped i18n parity, OKF wiki validation, and git diff --check pass. Installed Lowlight/Refractor grammars confirm the test token classes. Independent correctness re-check: PASS. Simplification review: PASS. The shared Docker engine became blocked by stale execs from another active worktree before the final filtered build and save/reload retry; no focused Playwright runtime or post-patch persistence pass is claimed here. | Commit Slice 4, then recover the container and run the full Slice 5 gates.`
437437
- `2026-07-10 | Slice 5 | DONE_WITH_CONCERNS | Slice 4 committed as d4b2247aa. Containerized repo typecheck, syncpack, AGENTS, and Prisma-sync gates pass; Playwright Prettier, tsc, and Chromium discovery pass (811 tests). The changed frontend-manage production build passes with NODE_ENV=production. Root check:all cannot run lint-staged inside the container because the mounted worktree .git file points to host-only metadata; its component gates were run separately. Container lint is additionally blocked by missing uv and eslint-plugin-react-hooks in the image. Three focused ZA runtime attempts reached global cleanup/seed but synthetic login redirected to auth under devrouter, 127.0.0.1, and localhost routing before any editor selector; stopped after three environment hypotheses. In-app Browser reconnected to the signed-in manage page after devrouter recovery, but Browser policy blocked the required post-restart reload, so no new save/reopen claim is made. The first explicit push ran the repository pre-push hook and completed the full root production build: 21/21 build tasks passed, including auth, frontend-manage, and frontend-pwa. | Use remote CI as the focused runtime gate; do not classify local harness failures as product failures.`
438438
- `2026-07-10 | Slice 6 | DONE_WITH_CONCERNS | Independent final review found focused external prop updates could be dropped and Markdown-significant indentation could compare equal. Fixed in 8975d2054 by removing the focus guard and trim; container Prettier and manage tsc pass, correctness review PASS_WITH_TEST_GAP, simplification PASS. Security review: raw HTML remains disabled, rehype-sanitize remains before Prism, code classes are restricted to language-*, dependency/lockfile changes are paired, scoped changed-file Opengrep reports 0 findings; the repo-wide auto scan reports 607 pre-existing findings and three analysis timeouts. Thermo review found concrete cross-app style drift; fixed manage preview table selectors and PWA generic pre selector in f4a2736c7. Thermo follow-up PASS; package-level shared CSS export explicitly deferred because it would add a new cross-package build/import contract. The documented .ProseMirror readiness wait remains based on measured layout shifts. Branch through ebdb9e6e2 pushed explicitly to the PR head; fresh remote checks are pending. Live PR remains REVIEW_REQUIRED; GitGuardian still flags the known dev-test credential commit inherited through v3 history. | Push this final evidence update, watch fresh CI, then refresh PR evidence/reviews.`
439+
- `2026-07-10 | CI closure | DONE_WITH_CONCERNS | Fresh run 29110571382 exposed two deterministic test defects. ZA targeted a non-focusable table-cell wrapper and saved empty cells; fixed in f2a6c791d by focusing the cell paragraph and asserting editor text before save. S-group-activity expected a second confirmation after production had cleared editing state; fixed in 9a4c805d8 by asserting the modal remains absent while downstream grading assertions verify the selected submission. Final run 29112473952 passed build-and-compile and all eight Playwright shards. All other code checks passed; GitGuardian remained the sole failing check. PR body and review threads were refreshed; unresolved thread count is zero. | Authorized GitGuardian disposition, screenshots, and human approval remain.`
440+
- `2026-07-10 | Branch review and simplification | DONE_WITH_CONCERNS | Two-axis review against origin/v3 found app-local import and missing data-cy hooks, stale evidence, explicit save/reopen/PWA evidence gaps, and pre-takeover AGENTS scope. Thermo pass removed redundant preventDefault calls from native type=button controls, simplified Markdown comparison syntax, restored the real optional-content boundary after independent review caught an undefined Formik caller, added stable hooks to new code/table controls, and strengthened ZA's add-column selector. Shared CSS extraction remains deferred because it would create a new package stylesheet contract. Pre-takeover AGENTS cleanup remains untouched as user-owned work. In-app Browser blocked the devrouter URL before page load, so no new browser claim is made. Targeted Prettier, frontend-manage tsc, Playwright tsc, test discovery, and diff checks pass locally; remote CI must rerun on the simplification commit. | Push simplification, watch fresh CI, then leave only external blockers.`
439441

440442
## Next Steps
441443

442-
1. Push explicitly to `origin/migrate-editor-to-tiptap` and watch fresh CI.
443-
2. Refresh review threads, GitGuardian disposition, screenshots, and whole-branch PR copy after CI.
444+
1. Commit and push the reviewed simplification; watch fresh CI.
445+
2. Obtain authorized GitGuardian disposition.
446+
3. Attach reviewer-visible desktop/narrow screenshots and record explicit editor reopen/PWA-route evidence.
447+
4. Obtain human approval; merge only after separate explicit user approval.

0 commit comments

Comments
 (0)