[Feat] #106 - Novel 스타일 슬래시 커맨드 RichEditor 구현#107
Conversation
# Conflicts: # src/shared/ui/form/form.stories.tsx # src/shared/ui/modal/modal.stories.tsx # src/views/login/index.tsx # tsconfig.json
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR introduces a Tiptap-based RichEditor component with slash-command functionality and supporting UI components. It adds seven new formatting icons (headings, lists, quote, code), implements the rich editor with keyboard-driven command menu, creates a slash-command Tiptap extension, and refactors existing modal story state patterns for consistency. ChangesModal Story Refactoring
Rich Editor with Slash Commands
Sequence DiagramsequenceDiagram
participant User
participant RichEditor as RichEditor
participant Tiptap as Tiptap Editor
participant SlashMenu as SlashCommand<br/>Menu
User->>RichEditor: Type "/" character
RichEditor->>Tiptap: "/" triggers suggestion
Tiptap->>RichEditor: onUpdate fires with/"query"
RichEditor->>RichEditor: Filter & populate<br/>menuState items
RichEditor->>SlashMenu: Render with items &<br/>selectedIndex=0
SlashMenu->>SlashMenu: Position menu &<br/>scroll to selection
User->>SlashMenu: Press ArrowDown
SlashMenu->>SlashMenu: Update selectedIndex
User->>SlashMenu: Press Enter
SlashMenu->>SlashMenu: Execute selected<br/>command
SlashMenu->>RichEditor: Call onSelect(item)
RichEditor->>Tiptap: command({ editor, range })
Tiptap->>RichEditor: Insert block &<br/>clear menuState
RichEditor->>User: Render updated content
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly Related PRs
Suggested Labels
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 4 minutes and 12 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/shared/ui/icons/index.tsx (1)
562-753: ⚡ Quick winConsider defaulting SVG icons to
aria-hidden="true".None of the new icons (nor the existing ones) suppress screen-reader announcement by default. When an icon appears alongside visible text — as it will inside the slash-command menu — screen readers may still announce the
<svg>element. Because{...props}is spread last, callers can already override this, but the burden falls on every use-site.A low-friction fix is to set
aria-hidden="true"on the<svg>root of each new icon (and existing ones, ideally as a follow-up sweep):♻️ Example for
Heading1Icon(apply the same pattern to all new icons)export const Heading1Icon: React.FC<IconProps> = ({ size = 20, ...props }) => ( <svg width={size} height={size} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" {...props} >The
{...props}spread still lets individual call-sites override witharia-hidden={false}+ an explicitaria-labelwhen an icon is used standalone.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/ui/icons/index.tsx` around lines 562 - 753, The SVG icons (Heading1Icon, Heading2Icon, Heading3Icon, ListBulletIcon, ListOrderedIcon, QuoteIcon, CodeIcon) should default to aria-hidden="true" to prevent unnecessary screen-reader announcements; update each component's <svg> root to include aria-hidden="true" (keeping the existing {...props} spread so callers can override with aria-hidden={false} and an aria-label when needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/shared/ui/rich-editor/rich-editor.tsx`:
- Around line 104-112: The command handler currently calls window.prompt() after
deleting the selection and passes unvalidated input to setImage; change it to
prompt before mutating the editor and add a minimal URL guard: extract the
prompt into a small helper (e.g., promptForImageUrl) so it can be mocked/tested
and feature-detected for sandboxed iframes, validate the returned value
(non-empty, reject javascript: scheme, require http/https or a safe data:
pattern) and only then call editor.chain().focus().deleteRange(range).setImage({
src: url }).run(); ensure you handle null/empty from the prompt by leaving the
editor untouched.
- Around line 241-243: The onUpdate callback passed into useEditor currently
closes over the initial onChange prop (onUpdate: ({ editor }) => {
onChange?.(editor.getHTML()); }), so later prop updates are ignored; fix this by
storing the latest onChange in a ref (e.g., onChangeRef) updated inside a
useEffect and call onChangeRef.current?.(editor.getHTML()) inside the useEditor
onUpdate handler so the editor always invokes the most recent onChange; update
references to onChange in useEditor/onUpdate to use the ref instead of the prop
directly.
- Line 172: Link.configure({ openOnClick: !isEditable }) captures isEditable at
initialization so links remain non-clickable when isEditable later changes;
instead set Link.configure({ openOnClick: false }) so it never freezes behavior,
and in the editor instance override click handling (use editorProps.handleClick
or handleClickOn) to detect current editor.isEditable (or the prop isEditable)
and manually open the link when the editor is read-only; keep the existing
useEffect that calls editor.setEditable(isEditable) and ensure the click handler
uses editor.getAttributes / node attrs to resolve and open the href when
appropriate.
In `@src/shared/ui/rich-editor/slash-command-menu.tsx`:
- Around line 56-84: Change the ARIA roles from a listbox/option pattern to a
menu/menuitem pattern: update the container that currently uses role="listbox"
(the div using menuClasses and positionStyle) to role="menu" and change each
item button that currently sets role="option" to role="menuitem"; keep the
existing selection handling (selectedIndex, aria-selected) or replace
aria-selected with aria-current/aria-checked only if semantically appropriate
for your menu items, and ensure itemRefs, getItemClasses, items, and onSelect
logic remain intact so keyboard focus and selection behavior are preserved.
---
Nitpick comments:
In `@src/shared/ui/icons/index.tsx`:
- Around line 562-753: The SVG icons (Heading1Icon, Heading2Icon, Heading3Icon,
ListBulletIcon, ListOrderedIcon, QuoteIcon, CodeIcon) should default to
aria-hidden="true" to prevent unnecessary screen-reader announcements; update
each component's <svg> root to include aria-hidden="true" (keeping the existing
{...props} spread so callers can override with aria-hidden={false} and an
aria-label when needed).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 162cf509-f1af-4de6-9b25-496b288316b1
📒 Files selected for processing (9)
src/shared/ui/icons/index.tsxsrc/shared/ui/index.tssrc/shared/ui/input/input.stories.tsxsrc/shared/ui/modal/modal.stories.tsxsrc/shared/ui/rich-editor/index.tssrc/shared/ui/rich-editor/rich-editor.stories.tsxsrc/shared/ui/rich-editor/rich-editor.tsxsrc/shared/ui/rich-editor/slash-command-menu.tsxsrc/shared/ui/rich-editor/slash-command.ts
…, 아이콘 aria-hidden)
🔎 What is this PR?
📝 Changes
src/shared/ui/rich-editor/rich-editor.tsx슬래시 커맨드 UX로 전면 재작성 (기존 툴바 제거)slash-command.ts:@tiptap/suggestion기반 Tiptap Extension + 검색 헬퍼 신규 작성slash-command-menu.tsx: 커서 기준으로 Portal 렌더되는 슬래시 메뉴 UI 신규 작성@/shared/ui/icons컨벤션 준수📚 Background / Context
@tiptap/suggestion기반으로 슬래시 커맨드를 직접 구현✔ Checklist
pnpm build)pnpm lint)Summary by CodeRabbit
Release Notes
New Features
Documentation