-
Notifications
You must be signed in to change notification settings - Fork 0
feat(frontend): 職務経歴書の入力モーダル改善(即時保存・自己PR/職務要約の専用入力UI) #310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
637d93c
feat(frontend): 職務経歴書の入力モーダルを改善(即時保存・専用入力UI)
claude 818329a
feat(frontend): 入力モーダルの文字数カウントをMarkdown記法除去後の表示テキストに変更
claude 88e1544
Merge origin/main into claude/project-modal-input-ui-nezQj
claude a093778
fix(frontend): レビュー指摘対応(モーダルa11y・メッセージSSoT・テスト堅牢化)
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import { test, expect, type Page } from "@playwright/test"; | ||
|
|
||
| import { UI_MESSAGES, charCountLabel } from "../src/constants/messages"; | ||
| import { setupAuth, waitForAuthenticatedLayout } from "./helpers/auth"; | ||
|
|
||
| /** | ||
| * 自己PR・職務要約の専用入力モーダル E2E。 | ||
| * | ||
| * これらのフィールドはフォーム本体ではプレビュー + 編集ボタンに集約され、入力は専用モーダルで行う。 | ||
| * シナリオ: | ||
| * 1. 「自己PRを編集」ボタンでモーダルが開き、現在値が入力欄に出る | ||
| * 2. 入力すると右下の文字数カウント(空白除外)が更新される | ||
| * 3. × で閉じると、入力値がフォーム側プレビューに反映されている | ||
| */ | ||
|
|
||
| /** 職務経歴書の最小 API モック(マスタ・最新取得)。 */ | ||
| async function setupResumeApi(page: Page) { | ||
| await page.route("**/api/master-data/qualification", (route) => | ||
| route.fulfill({ status: 200, contentType: "application/json", body: "[]" }), | ||
| ); | ||
| await page.route("**/api/master-data/technology-stack", (route) => | ||
| route.fulfill({ status: 200, contentType: "application/json", body: "[]" }), | ||
| ); | ||
| await page.route("**/api/resumes/latest", (route) => | ||
| route.fulfill({ | ||
| status: 200, | ||
| contentType: "application/json", | ||
| body: JSON.stringify({ | ||
| id: "resume-1", | ||
| full_name: "山田 太郎", | ||
| career_summary: "初期サマリー", | ||
| self_pr: "初期自己PR", | ||
| experiences: [], | ||
| qualifications: [], | ||
| }), | ||
| }), | ||
| ); | ||
| } | ||
|
|
||
| test.describe("職務経歴書 自己PR・職務要約モーダル", () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await setupAuth(page); | ||
| await setupResumeApi(page); | ||
| await page.goto("/career"); | ||
| await waitForAuthenticatedLayout(page); | ||
| }); | ||
|
|
||
| test("自己PRをモーダルで編集すると文字数カウントが更新されフォームへ反映される", async ({ | ||
| page, | ||
| }) => { | ||
| // フォーム本体には現在値のプレビューが出ている。 | ||
| await expect(page.getByText("初期自己PR")).toBeVisible(); | ||
|
|
||
| // 「自己PRを編集」ボタンでモーダルを開く。 | ||
| await page | ||
| .getByRole("button", { name: `${UI_MESSAGES.FIELD_SELF_PR}を${UI_MESSAGES.EDIT}` }) | ||
| .click(); | ||
|
|
||
| // モーダル内の入力欄(textarea は本フォームではモーダル内にのみ存在)に現在値がロードされている。 | ||
| const textarea = page.locator("textarea"); | ||
| await expect(textarea).toHaveValue("初期自己PR"); | ||
|
|
||
| // 入力すると右下の文字数カウント(空白除外)が更新される。 | ||
| await textarea.fill("新しい 自己PR 本文"); | ||
| // 空白を除くと「新しい自己PR本文」= 9 文字(新・し・い・自・己・P・R・本・文)。 | ||
| await expect(page.getByText(charCountLabel(9), { exact: true })).toBeVisible(); | ||
|
|
||
| // × で閉じる。 | ||
| await page.getByRole("button", { name: UI_MESSAGES.MODAL_CLOSE, exact: true }).click(); | ||
| await expect(textarea).toHaveCount(0); | ||
|
|
||
| // フォーム側プレビューに編集値が反映されている(即時に formCache へ反映)。 | ||
| await expect(page.getByText("新しい 自己PR 本文")).toBeVisible(); | ||
| }); | ||
|
|
||
| test("必須未入力で保存すると職務要約モーダルが自動で開く", async ({ page }) => { | ||
| // 職務要約を空にするため、まずモーダルを開いて全消しする。 | ||
| await page | ||
| .getByRole("button", { | ||
| name: `${UI_MESSAGES.FIELD_CAREER_SUMMARY}を${UI_MESSAGES.EDIT}`, | ||
| }) | ||
| .click(); | ||
| const textarea = page.locator("textarea"); | ||
| await textarea.fill(""); | ||
| await page.getByRole("button", { name: UI_MESSAGES.MODAL_CLOSE, exact: true }).click(); | ||
| await expect(textarea).toHaveCount(0); | ||
|
|
||
| // 保存(更新)ボタンを押すと、職務要約が未入力なのでモーダルが自動で開く。 | ||
| await page.getByRole("button", { name: /更新する|保存する/ }).click(); | ||
| // 開いたのが職務要約モーダルであることをタイトル(完全一致)で確認する。 | ||
| await expect(page.getByText(UI_MESSAGES.FIELD_CAREER_SUMMARY, { exact: true })).toBeVisible(); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { useFocusOnMatch } from "../../hooks/useFocusOnMatch"; | ||
| import type { UseResumeImportAssistReturn } from "../../hooks/career/useResumeImportAssist"; | ||
| import { CharCount } from "../ui/CharCount"; | ||
| import { ModalShell } from "../ui/ModalShell"; | ||
| import { MarkdownTextarea } from "./MarkdownTextarea"; | ||
|
|
||
| type Props = { | ||
| /** ヘッダー / 入力ラベルに使うフィールド名(例: 自己PR / 職務要約) */ | ||
| title: string; | ||
| /** Markdown 値 */ | ||
| value: string; | ||
| /** 値変更ハンドラ(親の onChangeField 経由で即時に formCache へ反映される) */ | ||
| onChange: (value: string) => void; | ||
| /** モーダルを閉じるコールバック(× / オーバーレイ共通) */ | ||
| onClose: () => void; | ||
| /** 取り込み補助。ファイルがあれば右カラムに原本ビューを再掲する */ | ||
| assist?: UseResumeImportAssistReturn; | ||
| /** バリデーション失敗フィールドとして強調&フォーカスするか */ | ||
| invalid?: boolean; | ||
| }; | ||
|
|
||
| /** | ||
| * 自己PR・職務要約のような単一 Markdown フィールドを専用 UI で入力させるモーダル。 | ||
| * 左に大きな入力欄、右に取り込み原本ビュー、右下に文字数カウント(空白除外)を表示する。 | ||
| * 保存ボタンは持たず、入力は即時に親(formCache)へ反映される。 | ||
| */ | ||
| export function MarkdownFieldModal({ title, value, onChange, onClose, assist, invalid }: Props) { | ||
| // バリデーション起因でモーダルが開かれた時、入力欄へフォーカスする。 | ||
| const textareaRef = useFocusOnMatch<HTMLTextAreaElement>(!!invalid); | ||
|
|
||
| return ( | ||
| <ModalShell title={title} onClose={onClose} assist={assist}> | ||
| <MarkdownTextarea | ||
| label={title} | ||
| value={value} | ||
| onChange={onChange} | ||
| rows={16} | ||
| fill | ||
| required | ||
| invalid={invalid} | ||
| textareaRef={textareaRef} | ||
| /> | ||
| <CharCount value={value} /> | ||
| </ModalShell> | ||
| ); | ||
| } |
64 changes: 64 additions & 0 deletions
64
frontend/src/components/forms/MarkdownFieldTrigger.module.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| .wrapper { | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 0.4rem; | ||
| } | ||
|
|
||
| .head { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| gap: 0.6rem; | ||
| } | ||
|
|
||
| /* 内容プレビュー兼編集ボタン。クリックでモーダルを開く。 */ | ||
| .trigger { | ||
| all: unset; | ||
| cursor: pointer; | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 0.6rem; | ||
| border: 1px solid var(--border-input, var(--border)); | ||
| border-radius: 6px; | ||
| padding: 0.55rem 0.7rem; | ||
| background: var(--bg-card); | ||
| min-height: 2.4rem; | ||
| } | ||
|
|
||
| .trigger:hover { | ||
| border-color: var(--accent); | ||
| } | ||
|
|
||
| .trigger.invalid { | ||
| border-color: var(--error); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| /* キーボード操作時のフォーカスを可視化する(all:unset で消えるネイティブ outline の代替)。 */ | ||
| .trigger:focus-visible { | ||
| outline: 2px solid var(--accent); | ||
| outline-offset: 2px; | ||
| } | ||
|
|
||
| /* 値のプレビュー(1 行に切り詰め)。 */ | ||
| .preview { | ||
| flex: 1; | ||
| min-width: 0; | ||
| color: var(--text-primary); | ||
| font-size: 0.9rem; | ||
| line-height: 1.5; | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| } | ||
|
|
||
| .empty { | ||
| color: var(--text-muted); | ||
| } | ||
|
|
||
| /* 「編集」リンク風ラベル。 */ | ||
| .editLabel { | ||
| flex-shrink: 0; | ||
| color: var(--accent); | ||
| font-size: 0.85rem; | ||
| font-weight: 600; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.