Let core-value meaning and signals be edited on first open with Add Value, confirm discard when closing without adding, and pin Keep editing to the footer back slot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-19 21:56:52 -06:00
co-authored by Cursor
parent d6d6a5e055
commit e9951f260b
9 changed files with 136 additions and 245 deletions
@@ -84,6 +84,7 @@ import { SignedInDraftHydration } from "./SignedInDraftHydration";
import { CreateFlowPendingAvatarFlush } from "./components/CreateFlowPendingAvatarFlush"; import { CreateFlowPendingAvatarFlush } from "./components/CreateFlowPendingAvatarFlush";
import Alert from "../../components/modals/Alert"; import Alert from "../../components/modals/Alert";
import Create from "../../components/modals/Create"; import Create from "../../components/modals/Create";
import { MODAL_FOOTER_START_SLOT_CLASS } from "../../components/modals/ModalFooter";
import Share from "../../components/modals/Share"; import Share from "../../components/modals/Share";
import { import {
CreateFlowDraftSaveBannerProvider, CreateFlowDraftSaveBannerProvider,
@@ -637,6 +638,7 @@ function CreateFlowLayoutContent({
nextButtonText={messages.create.topNav.leaveConfirmProceed} nextButtonText={messages.create.topNav.leaveConfirmProceed}
onNext={() => closeLeaveConfirm(true)} onNext={() => closeLeaveConfirm(true)}
footerContent={ footerContent={
<div className={MODAL_FOOTER_START_SLOT_CLASS}>
<Button <Button
buttonType="ghost" buttonType="ghost"
palette="default" palette="default"
@@ -645,6 +647,7 @@ function CreateFlowLayoutContent({
> >
{messages.create.topNav.leaveConfirmCancel} {messages.create.topNav.leaveConfirmCancel}
</Button> </Button>
</div>
} }
backdropVariant="blurredYellow" backdropVariant="blurredYellow"
ariaLabel={messages.create.topNav.leaveConfirmTitle} ariaLabel={messages.create.topNav.leaveConfirmTitle}
@@ -15,7 +15,7 @@ import type { CoreValueDetailEntry } from "../../types";
export interface CoreValueEditFieldsProps { export interface CoreValueEditFieldsProps {
value: CoreValueDetailEntry; value: CoreValueDetailEntry;
onChange: (_next: CoreValueDetailEntry) => void; onChange: (_next: CoreValueDetailEntry) => void;
/** View mode until the user taps **Customize**. */ /** Disable meaning/signals. Create-flow core-values omits this; final-review locks until Customize. */
readOnly?: boolean; readOnly?: boolean;
} }
@@ -8,7 +8,7 @@ import ContentLockup from "../../../../components/type/ContentLockup";
import { useMessages } from "../../../../contexts/MessagesContext"; import { useMessages } from "../../../../contexts/MessagesContext";
import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft"; import { buildCoreValueChipOptionsFromDraft } from "../../../../../lib/create/coreValueChipOptionsFromDraft";
import { useCreateFlow } from "../../context/CreateFlowContext"; import { useCreateFlow } from "../../context/CreateFlowContext";
import { useDiscardCustomizeConfirm } from "../../hooks/useDiscardCustomizeConfirm"; import { useAsyncConfirm } from "../../../../hooks/useAsyncConfirm";
import type { import type {
CommunityStructureChipSnapshotRow, CommunityStructureChipSnapshotRow,
CoreValueDetailEntry, CoreValueDetailEntry,
@@ -16,14 +16,7 @@ import type {
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup"; import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell"; import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
import { CoreValueEditFields } from "../../components/methodEditFields"; import { CoreValueEditFields } from "../../components/methodEditFields";
import MethodCardCustomizeModalHeader from "../../components/MethodCardCustomizeModalHeader";
import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu"; import { buildCustomRuleModalKebabMenu } from "../../components/customRuleModalKebabMenu";
import {
captureMethodCardCustomizeSnapshot,
isMethodCardCustomizeSessionDirty,
type MethodCardCustomizeSnapshot,
type MethodCardHeaderDraft,
} from "../../../../../lib/create/methodCardCustomizeSession";
import { import {
duplicateCoreValueChipInDraft, duplicateCoreValueChipInDraft,
MAX_SELECTED_CORE_VALUES, MAX_SELECTED_CORE_VALUES,
@@ -37,8 +30,7 @@ const MAX_CORE_VALUES = MAX_SELECTED_CORE_VALUES;
* Why three sessions, not two: * Why three sessions, not two:
* *
* - `pending` — preset chip just selected; modal opened to capture * - `pending` — preset chip just selected; modal opened to capture
* meaning/signals. Dismiss = unselect the chip (keep it in the * meaning/signals. Close (X) confirms, then unselects the chip.
* preset row, just not selected).
* - `customPending` — brand-new custom chip just created via the Add * - `customPending` — brand-new custom chip just created via the Add
* value flow; modal opened with empty fields. Dismiss = drop the * value flow; modal opened with empty fields. Dismiss = drop the
* chip entirely (it was never confirmed via the Add Value button). * chip entirely (it was never confirmed via the Add Value button).
@@ -101,13 +93,11 @@ export function CoreValuesSelectScreen() {
[cv.values], [cv.values],
); );
const { confirmDiscard, confirmDirtyCustomizeCancel, confirmDialog } = const { requestConfirm, confirmDialog } = useAsyncConfirm();
useDiscardCustomizeConfirm();
const { markCreateFlowInteraction, updateState, replaceState, state } = const { markCreateFlowInteraction, updateState, replaceState, state } =
useCreateFlow(); useCreateFlow();
const coreCustomizeSnapshotRef = const initialDraftRef = useRef<CoreValueDetailEntry | null>(null);
useRef<MethodCardCustomizeSnapshot<CoreValueDetailEntry> | null>(null);
const pendingEphemeralCoreDuplicateRef = useRef<string | null>(null); const pendingEphemeralCoreDuplicateRef = useRef<string | null>(null);
const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(() => const [coreValueOptions, setCoreValueOptions] = useState<ChipOption[]>(() =>
@@ -123,9 +113,6 @@ export function CoreValuesSelectScreen() {
); );
const [modalSession, setModalSession] = useState<ModalSession | null>(null); const [modalSession, setModalSession] = useState<ModalSession | null>(null);
const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL); const [draft, setDraft] = useState<CoreValueDetailEntry>(EMPTY_DETAIL);
const [modalEditUnlocked, setModalEditUnlocked] = useState(false);
const [customizeHeaderDraft, setCustomizeHeaderDraft] =
useState<MethodCardHeaderDraft | null>(null);
useEffect(() => { useEffect(() => {
setCoreValueOptions( setCoreValueOptions(
@@ -190,12 +177,11 @@ export function CoreValuesSelectScreen() {
valueLabel: string, valueLabel: string,
seedDetail?: CoreValueDetailEntry, seedDetail?: CoreValueDetailEntry,
) => { ) => {
setDraft(seedDetail ?? getInitialTexts(chipId, valueLabel)); const initial = seedDetail ?? getInitialTexts(chipId, valueLabel);
initialDraftRef.current = { ...initial };
setDraft(initial);
setActiveModalChipId(chipId); setActiveModalChipId(chipId);
setModalSession(session); setModalSession(session);
setModalEditUnlocked(false);
setCustomizeHeaderDraft(null);
coreCustomizeSnapshotRef.current = null;
markCreateFlowInteraction(); markCreateFlowInteraction();
}, },
[getInitialTexts, markCreateFlowInteraction], [getInitialTexts, markCreateFlowInteraction],
@@ -209,87 +195,36 @@ export function CoreValuesSelectScreen() {
[markCreateFlowInteraction], [markCreateFlowInteraction],
); );
const resetCustomizeSession = useCallback(() => {
coreCustomizeSnapshotRef.current = null;
setModalEditUnlocked(false);
setCustomizeHeaderDraft(null);
}, []);
const finalizeModalDismiss = useCallback(() => { const finalizeModalDismiss = useCallback(() => {
pendingEphemeralCoreDuplicateRef.current = null; pendingEphemeralCoreDuplicateRef.current = null;
resetCustomizeSession(); initialDraftRef.current = null;
setActiveModalChipId(null); setActiveModalChipId(null);
setModalSession(null); setModalSession(null);
}, [resetCustomizeSession]); }, []);
const handleCustomize = useCallback(() => { const confirmLeaveWithoutSaving = useCallback(async () => {
if (!activeModalChipId) return; const isPendingAdd =
const chipLabelNow = modalSession === "pending" || modalSession === "customPending";
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? ""; const initial = initialDraftRef.current;
if (!chipLabelNow) return; const editingDirty =
markCreateFlowInteraction(); modalSession === "editing" &&
const headerDraft: MethodCardHeaderDraft = { initial != null &&
title: chipLabelNow, (draft.meaning !== initial.meaning || draft.signals !== initial.signals);
description: "", if (!isPendingAdd && !editingDirty) {
}; return true;
coreCustomizeSnapshotRef.current = captureMethodCardCustomizeSnapshot(
draft,
null,
headerDraft,
);
setCustomizeHeaderDraft(headerDraft);
setModalEditUnlocked(true);
}, [activeModalChipId, coreValueOptions, draft, markCreateFlowInteraction]);
const handleCancelCustomize = useCallback(async () => {
if (!modalEditUnlocked) return;
const snap = coreCustomizeSnapshotRef.current;
if (!snap) {
resetCustomizeSession();
return;
} }
if ( return requestConfirm({
!(await confirmDirtyCustomizeCancel( title: cv.detailModal.discardTitle,
snap, description: isPendingAdd
draft, ? cv.detailModal.discardPendingDescription
null, : cv.detailModal.discardEditsDescription,
customizeHeaderDraft, proceedText: cv.detailModal.discardProceed,
)) cancelText: cv.detailModal.discardKeepEditing,
) { });
return; }, [cv.detailModal, draft, modalSession, requestConfirm]);
}
setDraft(structuredClone(snap.pendingDraft));
resetCustomizeSession();
}, [
confirmDirtyCustomizeCancel,
customizeHeaderDraft,
draft,
modalEditUnlocked,
resetCustomizeSession,
]);
const syncLabelFromCustomizeHeaderToOptions = useCallback(() => { const handleDuplicateCoreChip = useCallback(() => {
if (!activeModalChipId || !customizeHeaderDraft) return coreValueOptions;
const trimmed = customizeHeaderDraft.title.trim();
if (!trimmed) return coreValueOptions;
return coreValueOptions.map((opt) =>
opt.id === activeModalChipId ? { ...opt, label: trimmed } : opt,
);
}, [activeModalChipId, customizeHeaderDraft, coreValueOptions]);
const handleDuplicateCoreChip = useCallback(async () => {
if (!activeModalChipId || !modalSession) return; if (!activeModalChipId || !modalSession) return;
if (
!(await confirmDiscard(
modalEditUnlocked,
coreCustomizeSnapshotRef.current,
draft,
null,
customizeHeaderDraft,
))
) {
return;
}
markCreateFlowInteraction(); markCreateFlowInteraction();
const priorEphemeral = pendingEphemeralCoreDuplicateRef.current; const priorEphemeral = pendingEphemeralCoreDuplicateRef.current;
let outcome: ReturnType<typeof duplicateCoreValueChipInDraft> | null = null; let outcome: ReturnType<typeof duplicateCoreValueChipInDraft> | null = null;
@@ -312,7 +247,6 @@ export function CoreValuesSelectScreen() {
if (!outcome) { if (!outcome) {
return; return;
} }
resetCustomizeSession();
pendingEphemeralCoreDuplicateRef.current = outcome.newId; pendingEphemeralCoreDuplicateRef.current = outcome.newId;
openModal( openModal(
outcome.newId, outcome.newId,
@@ -322,30 +256,15 @@ export function CoreValuesSelectScreen() {
); );
}, [ }, [
activeModalChipId, activeModalChipId,
confirmDiscard,
customizeHeaderDraft,
draft, draft,
markCreateFlowInteraction, markCreateFlowInteraction,
modalEditUnlocked,
modalKebabMenu.duplicateTitleSuffix, modalKebabMenu.duplicateTitleSuffix,
modalSession, modalSession,
openModal, openModal,
replaceState, replaceState,
resetCustomizeSession,
]); ]);
const handleRemoveFromKebab = useCallback(async () => { const handleRemoveFromKebab = useCallback(() => {
if (
!(await confirmDiscard(
modalEditUnlocked,
coreCustomizeSnapshotRef.current,
draft,
null,
customizeHeaderDraft,
))
) {
return;
}
markCreateFlowInteraction(); markCreateFlowInteraction();
const ep = pendingEphemeralCoreDuplicateRef.current; const ep = pendingEphemeralCoreDuplicateRef.current;
@@ -386,28 +305,16 @@ export function CoreValuesSelectScreen() {
finalizeModalDismiss(); finalizeModalDismiss();
}, [ }, [
activeModalChipId, activeModalChipId,
confirmDiscard,
coreValueOptions, coreValueOptions,
customizeHeaderDraft,
draft,
finalizeModalDismiss, finalizeModalDismiss,
markCreateFlowInteraction, markCreateFlowInteraction,
modalEditUnlocked,
modalSession, modalSession,
persistCoreValues, persistCoreValues,
replaceState, replaceState,
]); ]);
const handleModalDismiss = useCallback(async () => { const handleModalDismiss = useCallback(async () => {
if ( if (!(await confirmLeaveWithoutSaving())) {
!(await confirmDiscard(
modalEditUnlocked,
coreCustomizeSnapshotRef.current,
draft,
null,
customizeHeaderDraft,
))
) {
return; return;
} }
@@ -436,51 +343,16 @@ export function CoreValuesSelectScreen() {
finalizeModalDismiss(); finalizeModalDismiss();
}, [ }, [
activeModalChipId, activeModalChipId,
confirmDiscard, confirmLeaveWithoutSaving,
coreValueOptions, coreValueOptions,
customizeHeaderDraft,
draft,
finalizeModalDismiss, finalizeModalDismiss,
modalEditUnlocked,
modalSession, modalSession,
persistCoreValues, persistCoreValues,
replaceState, replaceState,
]); ]);
const coreCustomizeSaveDisabled = useMemo(() => {
if (!modalEditUnlocked) return false;
const snap = coreCustomizeSnapshotRef.current;
if (!snap) return true;
return !isMethodCardCustomizeSessionDirty(
snap,
draft,
null,
customizeHeaderDraft,
);
}, [customizeHeaderDraft, draft, modalEditUnlocked]);
const handleModalConfirm = useCallback(() => { const handleModalConfirm = useCallback(() => {
if (!activeModalChipId || !modalSession) return; if (!activeModalChipId || !modalSession) return;
if (modalEditUnlocked && customizeHeaderDraft) {
if (coreCustomizeSaveDisabled) {
return;
}
markCreateFlowInteraction();
pendingEphemeralCoreDuplicateRef.current = null;
const nextOpts = syncLabelFromCustomizeHeaderToOptions();
persistCoreValues(nextOpts);
updateState({
coreValueDetailsByChipId: {
...(state.coreValueDetailsByChipId ?? {}),
[activeModalChipId]: draft,
},
});
resetCustomizeSession();
return;
}
if (modalSession === "pending" || modalSession === "customPending") {
markCreateFlowInteraction(); markCreateFlowInteraction();
pendingEphemeralCoreDuplicateRef.current = null; pendingEphemeralCoreDuplicateRef.current = null;
updateState({ updateState({
@@ -489,40 +361,21 @@ export function CoreValuesSelectScreen() {
[activeModalChipId]: draft, [activeModalChipId]: draft,
}, },
}); });
resetCustomizeSession(); finalizeModalDismiss();
setActiveModalChipId(null);
setModalSession(null);
}
}, [ }, [
activeModalChipId, activeModalChipId,
coreCustomizeSaveDisabled,
customizeHeaderDraft,
draft, draft,
finalizeModalDismiss,
markCreateFlowInteraction, markCreateFlowInteraction,
modalEditUnlocked,
modalSession, modalSession,
persistCoreValues,
resetCustomizeSession,
state.coreValueDetailsByChipId, state.coreValueDetailsByChipId,
syncLabelFromCustomizeHeaderToOptions,
updateState, updateState,
]); ]);
const modalChipLabel = const modalChipLabel =
coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? ""; coreValueOptions.find((o) => o.id === activeModalChipId)?.label ?? "";
const modalFieldsLocked = const showFooterPrimary = Boolean(modalSession);
!modalEditUnlocked &&
Boolean(
modalSession === "pending" ||
modalSession === "customPending" ||
modalSession === "editing",
);
const showFooterPrimary =
modalEditUnlocked ||
modalSession === "pending" ||
modalSession === "customPending";
const kebabMenuItems = useMemo(() => { const kebabMenuItems = useMemo(() => {
if (!modalSession || !activeModalChipId) return []; if (!modalSession || !activeModalChipId) return [];
@@ -530,8 +383,6 @@ export function CoreValuesSelectScreen() {
(o) => o.state === "selected", (o) => o.state === "selected",
).length; ).length;
return buildCustomRuleModalKebabMenu(modalKebabMenu, { return buildCustomRuleModalKebabMenu(modalKebabMenu, {
showCustomize: !modalEditUnlocked,
onCustomize: handleCustomize,
onDuplicate: onDuplicate:
modalSession !== "editing" || selectedCount >= MAX_CORE_VALUES modalSession !== "editing" || selectedCount >= MAX_CORE_VALUES
? undefined ? undefined
@@ -542,10 +393,8 @@ export function CoreValuesSelectScreen() {
}, [ }, [
activeModalChipId, activeModalChipId,
coreValueOptions, coreValueOptions,
handleCustomize,
handleDuplicateCoreChip, handleDuplicateCoreChip,
handleRemoveFromKebab, handleRemoveFromKebab,
modalEditUnlocked,
modalKebabMenu, modalKebabMenu,
modalSession, modalSession,
]); ]);
@@ -675,21 +524,6 @@ export function CoreValuesSelectScreen() {
onClose={handleModalDismiss} onClose={handleModalDismiss}
backdropVariant="blurredYellow" backdropVariant="blurredYellow"
headerContent={ headerContent={
modalEditUnlocked && customizeHeaderDraft ? (
<MethodCardCustomizeModalHeader
titleLabel={detailModal.customizeValueNameLabel}
descriptionLabel=""
titleValue={customizeHeaderDraft.title}
descriptionValue=""
onTitleChange={(title) =>
setCustomizeHeaderDraft((prev) =>
prev ? { ...prev, title } : null,
)
}
onDescriptionChange={() => {}}
showDescription={false}
/>
) : (
<div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0"> <div className="bg-[var(--color-surface-default-primary)] px-[24px] py-[12px] shrink-0">
<ContentLockup <ContentLockup
title={modalChipLabel} title={modalChipLabel}
@@ -698,18 +532,14 @@ export function CoreValuesSelectScreen() {
alignment="left" alignment="left"
/> />
</div> </div>
)
} }
showBackButton={modalEditUnlocked} showBackButton={false}
onBack={handleCancelCustomize}
backButtonText={modalKebabMenu.cancelCustomize}
showNextButton={showFooterPrimary} showNextButton={showFooterPrimary}
nextButtonDisabled={
modalEditUnlocked && coreCustomizeSaveDisabled
}
onNext={handleModalConfirm} onNext={handleModalConfirm}
nextButtonText={ nextButtonText={
modalEditUnlocked ? modalKebabMenu.saveEdits : detailModal.addValueButton modalSession === "editing"
? modalKebabMenu.saveEdits
: detailModal.addValueButton
} }
kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel} kebabTriggerAriaLabel={modalKebabMenu.triggerAriaLabel}
kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel} kebabMenuAriaLabel={modalKebabMenu.menuAriaLabel}
@@ -719,7 +549,6 @@ export function CoreValuesSelectScreen() {
ariaLabel={modalChipLabel || "Core value details"} ariaLabel={modalChipLabel || "Core value details"}
> >
<CoreValueEditFields <CoreValueEditFields
readOnly={modalFieldsLocked}
value={draft} value={draft}
onChange={handleDraftChange} onChange={handleDraftChange}
/> />
@@ -23,3 +23,7 @@ export interface ModalFooterProps {
footerContent?: React.ReactNode; footerContent?: React.ReactNode;
className?: string; className?: string;
} }
/** Left footer slot for confirm-dialog cancel; matches Back (`left` / `top` 16 / 12). */
export const MODAL_FOOTER_START_SLOT_CLASS =
"absolute left-[16px] top-[12px] flex min-h-[40px] items-center";
@@ -1,2 +1,3 @@
export { default } from "./ModalFooter.container"; export { default } from "./ModalFooter.container";
export type { ModalFooterProps } from "./ModalFooter.types"; export type { ModalFooterProps } from "./ModalFooter.types";
export { MODAL_FOOTER_START_SLOT_CLASS } from "./ModalFooter.types";
+3
View File
@@ -3,6 +3,7 @@
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import Button from "../components/buttons/Button"; import Button from "../components/buttons/Button";
import Create from "../components/modals/Create"; import Create from "../components/modals/Create";
import { MODAL_FOOTER_START_SLOT_CLASS } from "../components/modals/ModalFooter";
import type { CreateModalBackdropVariant } from "../components/modals/Create/CreateModalFrame.view"; import type { CreateModalBackdropVariant } from "../components/modals/Create/CreateModalFrame.view";
export type AsyncConfirmOptions = { export type AsyncConfirmOptions = {
@@ -58,6 +59,7 @@ export function useAsyncConfirm() {
nextButtonText={options.proceedText} nextButtonText={options.proceedText}
onNext={() => close(true)} onNext={() => close(true)}
footerContent={ footerContent={
<div className={MODAL_FOOTER_START_SLOT_CLASS}>
<Button <Button
buttonType="ghost" buttonType="ghost"
palette="default" palette="default"
@@ -66,6 +68,7 @@ export function useAsyncConfirm() {
> >
{options.cancelText} {options.cancelText}
</Button> </Button>
</div>
} }
backdropVariant={options.backdropVariant ?? "blurredYellow"} backdropVariant={options.backdropVariant ?? "blurredYellow"}
ariaLabel={options.ariaLabel ?? options.title} ariaLabel={options.ariaLabel ?? options.title}
@@ -13,7 +13,12 @@
"meaningLabel": "What does this value mean to your group?", "meaningLabel": "What does this value mean to your group?",
"signalsLabel": "Signals of Violation", "signalsLabel": "Signals of Violation",
"addValueButton": "Add Value", "addValueButton": "Add Value",
"customizeValueNameLabel": "Value name" "customizeValueNameLabel": "Value name",
"discardTitle": "Discard unsaved changes?",
"discardPendingDescription": "This value will not be added.",
"discardEditsDescription": "Your edits will not be saved.",
"discardProceed": "Discard",
"discardKeepEditing": "Keep editing"
}, },
"values": [ "values": [
{ {
@@ -263,7 +263,14 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
fireEvent.change(textboxes[2], { target: { value: "Edited principle" } }); fireEvent.change(textboxes[2], { target: { value: "Edited principle" } });
fireEvent.keyDown(document, { key: "Escape" }); fireEvent.keyDown(document, { key: "Escape" });
await screen.findByRole("button", { name: "Keep editing" }); const keepEditing = await screen.findByRole("button", {
name: "Keep editing",
});
expect(keepEditing.parentElement).toHaveClass(
"absolute",
"left-[16px]",
"top-[12px]",
);
await declineDiscardCustomizeEdits(); await declineDiscardCustomizeEdits();
expect(screen.getByRole("dialog")).toBeInTheDocument(); expect(screen.getByRole("dialog")).toBeInTheDocument();
@@ -9,6 +9,10 @@ describe("CoreValuesSelectScreen", () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
async function discardPendingValue() {
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
}
it("opens core value detail modal when a preset chip is clicked", async () => { it("opens core value detail modal when a preset chip is clicked", async () => {
renderWithProviders(<CoreValuesSelectScreen />); renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility")); fireEvent.click(screen.getByText("Accessibility"));
@@ -16,18 +20,41 @@ describe("CoreValuesSelectScreen", () => {
expect( expect(
within(dialog).getByRole("button", { name: "Add Value" }), within(dialog).getByRole("button", { name: "Add Value" }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(
screen.queryByRole("menuitem", { name: "Customize" }),
).not.toBeInTheDocument();
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
expect(
screen.queryByRole("menuitem", { name: "Customize" }),
).not.toBeInTheDocument();
}); });
it("closes modal and reverts pending selection when Escape is pressed", async () => { it("asks to discard when closing a pending value, then unselects on Discard", async () => {
renderWithProviders(<CoreValuesSelectScreen />); renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility")); fireEvent.click(screen.getByText("Accessibility"));
await screen.findByRole("dialog"); await screen.findByRole("dialog");
fireEvent.keyDown(document, { key: "Escape" }); fireEvent.keyDown(document, { key: "Escape" });
expect(
await screen.findByRole("button", { name: "Keep editing" }),
).toBeInTheDocument();
await discardPendingValue();
await waitFor(() => { await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
}); });
}); });
it("keeps the pending value modal open when Keep editing is chosen", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
await screen.findByRole("dialog");
fireEvent.keyDown(document, { key: "Escape" });
fireEvent.click(await screen.findByRole("button", { name: "Keep editing" }));
const dialog = await screen.findByRole("dialog");
expect(
within(dialog).getByRole("button", { name: "Add Value" }),
).toBeInTheDocument();
});
it("saves details when Add Value is clicked", async () => { it("saves details when Add Value is clicked", async () => {
renderWithProviders(<CoreValuesSelectScreen />); renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility")); fireEvent.click(screen.getByText("Accessibility"));
@@ -40,6 +67,17 @@ describe("CoreValuesSelectScreen", () => {
}); });
}); });
it("opens meaning and signals fields editable without Customize", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility"));
const dialog = await screen.findByRole("dialog");
const fields = within(dialog).getAllByRole("textbox");
expect(fields.length).toBeGreaterThanOrEqual(2);
for (const field of fields) {
expect(field).toBeEnabled();
}
});
// The "Add value" → custom-chip → modal flow uses a `customPending` // The "Add value" → custom-chip → modal flow uses a `customPending`
// session: dismissing the modal must drop the brand-new chip entirely // session: dismissing the modal must drop the brand-new chip entirely
// (not just unselect it), because the user never confirmed it via // (not just unselect it), because the user never confirmed it via
@@ -80,6 +118,7 @@ describe("CoreValuesSelectScreen", () => {
expect(countCustomChips(CUSTOM_LABEL)).toBe(1); expect(countCustomChips(CUSTOM_LABEL)).toBe(1);
fireEvent.keyDown(document, { key: "Escape" }); fireEvent.keyDown(document, { key: "Escape" });
fireEvent.click(await screen.findByRole("button", { name: "Discard" }));
await waitFor(() => { await waitFor(() => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
}); });