Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import {
type CareerExperienceForm,
type CareerProjectForm,
} from "../../../payloadBuilders";
import { UI_MESSAGES } from "../../../constants/messages";
import shared from "../../../styles/shared.module.css";
import styles from "../CareerResumeForm.module.css";
import { Collapsible } from "../../ui/Collapsible";
import { DeleteIconButton } from "../../ui/DeleteIconButton";
import { DirtyDot } from "../../ui/DirtyDot";
import { PlusIcon } from "../../icons/PlusIcon";
import { ClientEditor } from "./ClientEditor";

/** CareerExperienceEditor のプロパティ型 */
Expand Down Expand Up @@ -84,6 +87,12 @@ export function CareerExperienceEditor({
<DirtyDot visible={Boolean(dirty?.any)} />
</>
}
headerActions={
<DeleteIconButton
label={UI_MESSAGES.RESUME_DELETE_EXPERIENCE}
onClick={() => onRemoveExperience(expIndex)}
/>
}
>
{/* 会社名:事業内容:IT企業 = 4.5:5:0.5 の幅比で配置 */}
<div className={shared.inline} style={{ gridTemplateColumns: "4.5fr 5fr 0.5fr" }}>
Expand Down Expand Up @@ -276,15 +285,16 @@ export function CareerExperienceEditor({
projectSummary={projectSummary}
/>
))}
<button type="button" className="ghost" onClick={() => onAddClient(expIndex)}>
<button
type="button"
className={`ghost ${styles.addButton}`}
onClick={() => onAddClient(expIndex)}
>
<PlusIcon />
取引先を追加
</button>
</div>
)}

<button type="button" className="danger" onClick={() => onRemoveExperience(expIndex)}>
職務経歴を削除
</button>
</Collapsible>
</div>
);
Expand Down
28 changes: 13 additions & 15 deletions frontend/src/components/forms/CareerFormEditors/ClientEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import {
type CareerClientForm,
type CareerProjectForm,
} from "../../../payloadBuilders";
import { UI_MESSAGES } from "../../../constants/messages";
import shared from "../../../styles/shared.module.css";
import styles from "../CareerResumeForm.module.css";
import { DeleteIconButton } from "../../ui/DeleteIconButton";
import { DirtyDot } from "../../ui/DirtyDot";
import { PlusIcon } from "../../icons/PlusIcon";

/** ClientEditor のプロパティ型 */
type ClientEditorProps = {
Expand Down Expand Up @@ -104,6 +107,11 @@ export function ClientEditor({
<DirtyDot visible={Boolean(dirty?.self)} />
</span>
</label>
<DeleteIconButton
label={client.is_vacation ? UI_MESSAGES.RESUME_DELETE_VACATION : UI_MESSAGES.RESUME_DELETE_CLIENT}
onClick={() => onRemoveClient(expIndex, clientIndex)}
className={styles.headerDelete}
/>
</div>

{client.is_vacation ? (
Expand Down Expand Up @@ -203,34 +211,24 @@ export function ClientEditor({
>
編集
</button>
<button
type="button"
className="danger"
<DeleteIconButton
label={UI_MESSAGES.RESUME_DELETE_PROJECT}
onClick={() => onRemoveProject(expIndex, clientIndex, projIndex)}
>
削除
</button>
/>
</div>
</div>
);
})}
<button
type="button"
className="ghost"
className={`ghost ${styles.addButton}`}
onClick={() => onOpenProjectModal(expIndex, clientIndex, null)}
>
<PlusIcon />
プロジェクトを追加
</button>
</div>
)}

<button
type="button"
className="danger"
onClick={() => onRemoveClient(expIndex, clientIndex)}
>
{client.is_vacation ? "休業を削除" : "取引先を削除"}
</button>
</div>
);
}
25 changes: 25 additions & 0 deletions frontend/src/components/forms/CareerResumeForm.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,31 @@
flex-wrap: wrap;
}

/* 取引先見出しの削除アイコンを行右端に寄せる。入力欄の下端に高さを合わせる。 */
.headerDelete {
margin-left: auto;
margin-bottom: 0.4rem;
}

/* 資格1行: 入力フィールド群(資格名+取得日)を flex:1 で広げ、右端に削除アイコンを上寄せで置く。 */
.qualificationRow {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}

.qualificationRow > :first-child {
flex: 1;
min-width: 0;
}

/* アイコン+テキストの追加ボタン。アイコンとラベルを縦中央で揃える。 */
.addButton {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}

.clientNameLabel {
flex: 1;
min-width: 200px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import { useCareerExperienceMutators } from "../../../hooks/career/useCareerExpe
import { useProjectModalState } from "../../../hooks/career/useProjectModalState";
import type { UseResumeImportAssistReturn } from "../../../hooks/career/useResumeImportAssist";
import shared from "../../../styles/shared.module.css";
import styles from "../CareerResumeForm.module.css";
import { CareerExperienceEditor } from "../CareerFormEditors/CareerExperienceEditor";
import { ProjectModal } from "../ProjectModal";
import { DirtyDot } from "../../ui/DirtyDot";
import { PlusIcon } from "../../icons/PlusIcon";

/** CareerExperienceSection のプロパティ型 */
type CareerExperienceSectionProps = {
Expand Down Expand Up @@ -117,7 +119,12 @@ export function CareerExperienceSection({
/>
))}

<button type="button" className="ghost" onClick={mutators.addExperience}>
<button
type="button"
className={`ghost ${styles.addButton}`}
onClick={mutators.addExperience}
>
<PlusIcon />
職務経歴を追加
</button>
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { blankResumeQualification } from "../../../constants";
import type { QualificationDirty } from "../../../hooks/career/useCareerDirty";
import type { CareerFormState } from "../../../payloadBuilders";
import type { ResumeQualificationItem } from "../../../api/types";
import { UI_MESSAGES } from "../../../constants/messages";
import shared from "../../../styles/shared.module.css";
import styles from "../CareerResumeForm.module.css";
import { Collapsible } from "../../ui/Collapsible";
import { DeleteIconButton } from "../../ui/DeleteIconButton";
import { DirtyDot } from "../../ui/DirtyDot";
import { Skeleton } from "../../ui/Skeleton";
import { PlusIcon } from "../../icons/PlusIcon";
import { Combobox } from "../Combobox";

/** CareerQualificationsSection のプロパティ型 */
Expand Down Expand Up @@ -87,43 +91,47 @@ export function CareerQualificationsSection({
const rowDirty = qualificationsDirty?.[index];
return (
<div key={`qualification-${index}`} className={shared.entry}>
{/* 資格名:取得日 = 7:3 の幅比で配置 */}
<div className={shared.inline} style={{ gridTemplateColumns: "7fr 3fr" }}>
<label>
<span className={shared.labelText}>
資格名
<span className={shared.requiredBadge}>必須</span>
※プルダウンにないものはテキストで入力できます。
<DirtyDot visible={Boolean(rowDirty?.fields.name)} />
</span>
<Combobox
value={qualification.name}
onChange={(val) => updateField(index, "name", val)}
options={qualificationNames}
placeholder="例: 基本情報技術者試験"
allowCustom
/>
</label>
<label>
<span className={shared.labelText}>
取得日
<span className={shared.requiredBadge}>必須</span>
<DirtyDot visible={Boolean(rowDirty?.fields.acquired_date)} />
</span>
<input
type="month"
value={qualification.acquired_date}
onChange={(e) => updateField(index, "acquired_date", e.target.value)}
/>
</label>
<div className={styles.qualificationRow}>
{/* 資格名:取得日 = 7:3 の幅比で配置 */}
<div className={shared.inline} style={{ gridTemplateColumns: "7fr 3fr" }}>
<label>
<span className={shared.labelText}>
資格名
<span className={shared.requiredBadge}>必須</span>
※プルダウンにないものはテキストで入力できます。
<DirtyDot visible={Boolean(rowDirty?.fields.name)} />
</span>
<Combobox
value={qualification.name}
onChange={(val) => updateField(index, "name", val)}
options={qualificationNames}
placeholder="例: 基本情報技術者試験"
allowCustom
/>
</label>
<label>
<span className={shared.labelText}>
取得日
<span className={shared.requiredBadge}>必須</span>
<DirtyDot visible={Boolean(rowDirty?.fields.acquired_date)} />
</span>
<input
type="month"
value={qualification.acquired_date}
onChange={(e) => updateField(index, "acquired_date", e.target.value)}
/>
</label>
</div>
<DeleteIconButton
label={UI_MESSAGES.RESUME_DELETE_QUALIFICATION}
onClick={() => removeRow(index)}
/>
</div>
<button type="button" className="danger" onClick={() => removeRow(index)}>
資格を削除
</button>
</div>
);
})}
<button type="button" className="ghost" onClick={addRow}>
<button type="button" className={`ghost ${styles.addButton}`} onClick={addRow}>
<PlusIcon />
資格を追加
</button>
</>
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/components/icons/PlusIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function PlusIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
width="14"
height="14"
fill="currentColor"
className={className}
aria-hidden="true"
>
<path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H48c-17.7 0-32 14.3-32 32s14.3 32 32 32H192V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H400c17.7 0 32-14.3 32-32s-14.3-32-32-32H256V80z" />
</svg>
);
}
15 changes: 15 additions & 0 deletions frontend/src/components/icons/TrashIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function TrashIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
width="14"
height="14"
fill="currentColor"
className={className}
aria-hidden="true"
>
<path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z" />
</svg>
);
}
26 changes: 26 additions & 0 deletions frontend/src/components/ui/DeleteIconButton.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* 項目見出し右端に置く、アイコンのみの削除ボタン。
テキストの danger ボタンと違い、背景透明・小型でレイアウトを圧迫しない。 */
.deleteIcon {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
padding: 0;
border: none;
border-radius: 6px;
background: transparent;
color: var(--danger-text);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}

.deleteIcon:hover {
background: var(--danger-bg);
}

.deleteIcon:focus-visible {
outline: 2px solid var(--danger-text);
outline-offset: 1px;
}
36 changes: 36 additions & 0 deletions frontend/src/components/ui/DeleteIconButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";

import { DeleteIconButton } from "./DeleteIconButton";

describe("DeleteIconButton", () => {
/** label が aria-label として描画され、アクセシブルネームで参照できる */
it("label が aria-label として描画される", () => {
render(<DeleteIconButton label="職務経歴を削除" onClick={() => {}} />);
expect(screen.getByRole("button", { name: "職務経歴を削除" })).toBeInTheDocument();
});

/** クリックで onClick が呼ばれる */
it("クリックで onClick が呼ばれる", () => {
const onClick = vi.fn();
render(<DeleteIconButton label="取引先を削除" onClick={onClick} />);

fireEvent.click(screen.getByRole("button", { name: "取引先を削除" }));
expect(onClick).toHaveBeenCalledTimes(1);
});

/** クリックは親要素へ伝播しない(見出し内のトグルを誤発火させない) */
it("クリックが親要素へ伝播しない", () => {
const onParentClick = vi.fn();
const onClick = vi.fn();
render(
<div onClick={onParentClick}>
<DeleteIconButton label="資格を削除" onClick={onClick} />
</div>,
);

fireEvent.click(screen.getByRole("button", { name: "資格を削除" }));
expect(onClick).toHaveBeenCalledTimes(1);
expect(onParentClick).not.toHaveBeenCalled();
});
});
Loading
Loading