Open create-flow method modals with editable fields and tertiary default copy.

Seeded section text should look like the form default state, not locked primary, and Customize stays on the kebab instead of unlocking the body.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-28 12:59:53 -06:00
co-authored by Cursor
parent 744a85a44e
commit a820739d07
13 changed files with 153 additions and 11 deletions
@@ -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;
/** Disable meaning/signals. Create-flow core-values omits this; final-review locks until Customize. */ /** Disable meaning/signals. Facet and final-review callers omit this so fields stay editable on open. */
readOnly?: boolean; readOnly?: boolean;
} }
@@ -10,8 +10,8 @@
* *
* Card click opens the Figma create modal (node `20246-15829`) with three * Card click opens the Figma create modal (node `20246-15829`) with three
* editable sections rendered by {@link CommunicationMethodEditFields}. The primary * editable sections rendered by {@link CommunicationMethodEditFields}. The primary
* action is **Add Platform** for an unselected card; a selected card in view mode has * action is **Add Platform** for an unselected card and **Save** for a selected
* no footer primary — **Remove** is available from the kebab (same behavior as legacy * card. **Remove** is available from the kebab (same behavior as legacy
* footer remove via {@link removeMethodCardFromFacetSelection}). * footer remove via {@link removeMethodCardFromFacetSelection}).
*/ */
@@ -216,7 +216,7 @@ export function CommunicationMethodsScreen() {
methodId: pendingCardId, methodId: pendingCardId,
meta: state.customMethodCardMetaById, meta: state.customMethodCardMetaById,
fieldBlocksById: state.customMethodCardFieldBlocksById, fieldBlocksById: state.customMethodCardFieldBlocksById,
modalEditUnlocked: false, modalEditUnlocked: true,
draftFieldBlocks, draftFieldBlocks,
customFacetDetailsMatchPreset, customFacetDetailsMatchPreset,
}), }),
@@ -217,7 +217,7 @@ export function ConflictManagementScreen() {
methodId: pendingCardId, methodId: pendingCardId,
meta: state.customMethodCardMetaById, meta: state.customMethodCardMetaById,
fieldBlocksById: state.customMethodCardFieldBlocksById, fieldBlocksById: state.customMethodCardFieldBlocksById,
modalEditUnlocked: false, modalEditUnlocked: true,
draftFieldBlocks, draftFieldBlocks,
customFacetDetailsMatchPreset, customFacetDetailsMatchPreset,
}), }),
@@ -214,7 +214,7 @@ export function MembershipMethodsScreen() {
methodId: pendingCardId, methodId: pendingCardId,
meta: state.customMethodCardMetaById, meta: state.customMethodCardMetaById,
fieldBlocksById: state.customMethodCardFieldBlocksById, fieldBlocksById: state.customMethodCardFieldBlocksById,
modalEditUnlocked: false, modalEditUnlocked: true,
draftFieldBlocks, draftFieldBlocks,
customFacetDetailsMatchPreset, customFacetDetailsMatchPreset,
}), }),
@@ -250,7 +250,7 @@ export function DecisionApproachesScreen() {
methodId: pendingCardId, methodId: pendingCardId,
meta: state.customMethodCardMetaById, meta: state.customMethodCardMetaById,
fieldBlocksById: state.customMethodCardFieldBlocksById, fieldBlocksById: state.customMethodCardFieldBlocksById,
modalEditUnlocked: false, modalEditUnlocked: true,
draftFieldBlocks, draftFieldBlocks,
customFacetDetailsMatchPreset, customFacetDetailsMatchPreset,
}), }),
@@ -80,7 +80,8 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
}, },
}; };
// State styles (embedded: Figma 20736-12668 borderless, darker grey block, white text) // Embedded (Figma 20736-12668): borderless grey block; default copy is
// tertiary, primary on focus so seeded modal fields stay muted until edit.
const getStateStyles = (): { const getStateStyles = (): {
textarea: string; textarea: string;
label: string; label: string;
@@ -89,13 +90,13 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
if (disabled) { if (disabled) {
return { return {
textarea: textarea:
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] cursor-not-allowed opacity-60", "border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-tertiary,#b4b4b4)] cursor-not-allowed opacity-60",
label: "text-[var(--color-content-default-secondary)]", label: "text-[var(--color-content-default-secondary)]",
}; };
} }
return { return {
textarea: textarea:
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-default-tertiary)] focus:ring-inset", "border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-tertiary,#b4b4b4)] placeholder:text-[var(--color-content-default-tertiary,#b4b4b4)] focus:text-[var(--color-content-default-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-default-tertiary)] focus:ring-inset",
label: "text-[var(--color-content-default-secondary)]", label: "text-[var(--color-content-default-secondary)]",
}; };
} }
@@ -48,7 +48,8 @@ export interface TextAreaProps extends Omit<
showHelpIcon?: boolean; showHelpIcon?: boolean;
/** /**
* Visual appearance. "embedded" matches Create modal sections (Figma 20736-12668): * Visual appearance. "embedded" matches Create modal sections (Figma 20736-12668):
* borderless, darker grey background, white text. "default" is standard bordered input. * borderless, darker grey background, tertiary text in default (primary on
* focus). "default" is the standard bordered input.
* @default "default" * @default "default"
*/ */
appearance?: TextAreaAppearanceValue; appearance?: TextAreaAppearanceValue;
@@ -29,6 +29,9 @@ import { isCustomMethodCardId } from "./isCustomMethodCardId";
* so meta-only wizard cards show policy copy instead of empty section editors. * so meta-only wizard cards show policy copy instead of empty section editors.
* Pass `customFacetDetailsMatchPreset: false` when the caller knows facet details * Pass `customFacetDetailsMatchPreset: false` when the caller knows facet details
* were edited or cloned from a filled preset. * were edited or cloned from a filled preset.
*
* Create-flow facet screens and final-review pass `modalEditUnlocked: true` so
* section fields stay editable on open (Customize is the kebab wizard).
*/ */
export function usesWizardFieldBlocksModalBody(args: { export function usesWizardFieldBlocksModalBody(args: {
methodId: string; methodId: string;
@@ -0,0 +1,37 @@
import { describe, it, expect, afterEach } from "vitest";
import {
renderWithProviders as render,
screen,
cleanup,
within,
} from "../utils/test-utils";
import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { ConflictManagementScreen } from "../../app/(app)/create/screens/card/ConflictManagementScreen";
afterEach(() => {
cleanup();
});
describe("ConflictManagementScreen", () => {
it("opens section fields editable without Customize", async () => {
render(<ConflictManagementScreen />);
fireEvent.click(
screen.getAllByRole("button", {
name: /Peer Mediation: Trained members/,
})[0],
);
const dialog = await screen.findByRole("dialog");
const fields = within(dialog).getAllByRole("textbox");
expect(fields.length).toBeGreaterThan(0);
for (const field of fields) {
expect(field).toBeEnabled();
}
fireEvent.click(
within(dialog).getByRole("button", { name: "More options" }),
);
expect(
screen.getByRole("menuitem", { name: "Customize" }),
).toBeInTheDocument();
});
});
@@ -0,0 +1,37 @@
import { describe, it, expect, afterEach } from "vitest";
import {
renderWithProviders as render,
screen,
cleanup,
within,
} from "../utils/test-utils";
import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { MembershipMethodsScreen } from "../../app/(app)/create/screens/card/MembershipMethodsScreen";
afterEach(() => {
cleanup();
});
describe("MembershipMethodsScreen", () => {
it("opens section fields editable without Customize", async () => {
render(<MembershipMethodsScreen />);
fireEvent.click(
screen.getAllByRole("button", {
name: /Open Access: Maximum inclusion/,
})[0],
);
const dialog = await screen.findByRole("dialog");
const fields = within(dialog).getAllByRole("textbox");
expect(fields.length).toBeGreaterThan(0);
for (const field of fields) {
expect(field).toBeEnabled();
}
fireEvent.click(
within(dialog).getByRole("button", { name: "More options" }),
);
expect(
screen.getByRole("menuitem", { name: "Customize" }),
).toBeInTheDocument();
});
});
+13
View File
@@ -42,4 +42,17 @@ describe("TextArea appearance", () => {
expect(textarea).toBeInTheDocument(); expect(textarea).toBeInTheDocument();
expect(textarea).toHaveClass("border-0"); expect(textarea).toHaveClass("border-0");
}); });
it("uses tertiary text in the embedded default state and primary on focus", () => {
renderWithProviders(
<TextArea label="Notes" value="Some text" appearance="embedded" />,
);
const textarea = screen.getByRole("textbox", { name: /notes/i });
expect(textarea).toHaveClass(
"text-[var(--color-content-default-tertiary,#b4b4b4)]",
);
expect(textarea).toHaveClass(
"focus:text-[var(--color-content-default-primary)]",
);
});
}); });
@@ -126,6 +126,33 @@ describe("Create flow communication-methods page", () => {
expect(screen.getByRole("menuitem", { name: "Customize" })).toBeInTheDocument(); expect(screen.getByRole("menuitem", { name: "Customize" })).toBeInTheDocument();
}); });
test("meta-only custom policy opens with editable section fields", async () => {
const user = userEvent.setup();
render(
<CommunicationMethodsScreenWithState
initial={{
selectedCommunicationMethodIds: [CUSTOM_POLICY_ID],
customMethodCardMetaById: {
[CUSTOM_POLICY_ID]: { label: "My policy", supportText: "Desc" },
},
}}
/>,
);
await user.click(
screen.getAllByRole("button", { name: /My policy: Desc/ })[0],
);
const dialog = await screen.findByRole("dialog");
const textboxes = within(dialog).getAllByRole("textbox");
expect(textboxes.length).toBeGreaterThan(0);
for (const field of textboxes) {
expect(field).not.toBeDisabled();
}
expect(
within(dialog).queryByText("No custom fields yet."),
).not.toBeInTheDocument();
});
test("renders without error", () => { test("renders without error", () => {
render(<CommunicationMethodsScreen />); render(<CommunicationMethodsScreen />);
+23
View File
@@ -201,6 +201,29 @@ describe("Create flow decision-approaches page", () => {
).toHaveTextContent("SELECTED"); ).toHaveTextContent("SELECTED");
}); });
test("opens approach fields editable without Customize", async () => {
const user = userEvent.setup();
render(<DecisionApproachesScreen />);
await user.click(
screen.getByRole("button", {
name: /Lazy Consensus: A decision is assumed approved/,
}),
);
const dialog = await screen.findByRole("dialog");
const textboxes = within(dialog).getAllByRole("textbox");
expect(textboxes.length).toBeGreaterThan(0);
for (const field of textboxes) {
expect(field).not.toBeDisabled();
}
await user.click(
within(dialog).getByRole("button", { name: "More options" }),
);
expect(
screen.getByRole("menuitem", { name: "Customize" }),
).toBeInTheDocument();
});
test("clicking a card opens the create modal and confirming selects it", async () => { test("clicking a card opens the create modal and confirming selects it", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<DecisionApproachesScreen />); render(<DecisionApproachesScreen />);