Merge pull request 'Give MultiSelect chips accessible names and start creation fields empty.' (#85) from adilallo/fix/CR-220-CR-209-chip-names-creation-modal into main

Reviewed-on: #85
This commit was merged in pull request #85.
This commit is contained in:
an.di committed 2026-10-01 15:52:23 +00:00
commit 2fc7fbf818
35 files changed
+498 -156

No files matched your search

@@ -35,6 +35,10 @@ import {
conflictManagementPresetFor, conflictManagementPresetFor,
coreValuePresetFor, coreValuePresetFor,
decisionApproachPresetFor, decisionApproachPresetFor,
emptyCommunicationDetails,
emptyConflictManagementDetails,
emptyDecisionApproachTextFields,
emptyMembershipDetails,
membershipPresetFor, membershipPresetFor,
} from "../../../../lib/create/finalReviewChipPresets"; } from "../../../../lib/create/finalReviewChipPresets";
import { isCustomMethodCardId } from "../../../../lib/create/isCustomMethodCardId"; import { isCustomMethodCardId } from "../../../../lib/create/isCustomMethodCardId";
@@ -1129,6 +1133,7 @@ export function FinalReviewChipEditModal({
<CoreValueEditFields <CoreValueEditFields
value={draft.value} value={draft.value}
onChange={(value) => setDraft({ groupKey: "coreValues", value })} onChange={(value) => setDraft({ groupKey: "coreValues", value })}
generateDraft={coreValuePresetFor(target.overrideKey)}
/> />
))} ))}
{draft?.groupKey === "communication" && {draft?.groupKey === "communication" &&
@@ -1156,6 +1161,7 @@ export function FinalReviewChipEditModal({
onChange={(value) => onChange={(value) =>
setDraft({ groupKey: "communication", value }) setDraft({ groupKey: "communication", value })
} }
generateDraft={communicationPresetFor(target.overrideKey)}
/> />
))} ))}
{draft?.groupKey === "membership" && {draft?.groupKey === "membership" &&
@@ -1183,6 +1189,7 @@ export function FinalReviewChipEditModal({
onChange={(value) => onChange={(value) =>
setDraft({ groupKey: "membership", value }) setDraft({ groupKey: "membership", value })
} }
generateDraft={membershipPresetFor(target.overrideKey)}
/> />
))} ))}
{draft?.groupKey === "decisionApproaches" && {draft?.groupKey === "decisionApproaches" &&
@@ -1210,6 +1217,7 @@ export function FinalReviewChipEditModal({
onChange={(value) => onChange={(value) =>
setDraft({ groupKey: "decisionApproaches", value }) setDraft({ groupKey: "decisionApproaches", value })
} }
generateDraft={decisionApproachPresetFor(target.overrideKey)}
/> />
))} ))}
{draft?.groupKey === "conflictManagement" && {draft?.groupKey === "conflictManagement" &&
@@ -1237,6 +1245,7 @@ export function FinalReviewChipEditModal({
onChange={(value) => onChange={(value) =>
setDraft({ groupKey: "conflictManagement", value }) setDraft({ groupKey: "conflictManagement", value })
} }
generateDraft={conflictManagementPresetFor(target.overrideKey)}
/> />
))} ))}
</div> </div>
@@ -1324,52 +1333,63 @@ function seedDraftForTarget(
switch (target.groupKey) { switch (target.groupKey) {
case "coreValues": { case "coreValues": {
const saved = state.coreValueDetailsByChipId?.[target.overrideKey]; const saved = state.coreValueDetailsByChipId?.[target.overrideKey];
const preset = coreValuePresetFor(target.overrideKey); if (saved) {
return {
groupKey: "coreValues",
value: {
meaning: saved.meaning,
signals: saved.signals,
...(saved.supportText ? { supportText: saved.supportText } : {}),
},
};
}
return { return {
groupKey: "coreValues", groupKey: "coreValues",
value: { value: { meaning: "", signals: "" },
meaning: saved?.meaning ?? preset.meaning,
signals: saved?.signals ?? preset.signals,
...(saved?.supportText ? { supportText: saved.supportText } : {}),
},
}; };
} }
case "communication": { case "communication": {
const saved = const saved = state.communicationMethodDetailsById?.[target.overrideKey];
state.communicationMethodDetailsById?.[target.overrideKey] ?? return {
communicationPresetFor(target.overrideKey); groupKey: "communication",
return { groupKey: "communication", value: { ...saved } }; value: { ...(saved ?? emptyCommunicationDetails()) },
};
} }
case "membership": { case "membership": {
const saved = const saved = state.membershipMethodDetailsById?.[target.overrideKey];
state.membershipMethodDetailsById?.[target.overrideKey] ?? return {
membershipPresetFor(target.overrideKey); groupKey: "membership",
return { groupKey: "membership", value: { ...saved } }; value: { ...(saved ?? emptyMembershipDetails()) },
};
} }
case "decisionApproaches": { case "decisionApproaches": {
const saved = const saved = state.decisionApproachDetailsById?.[target.overrideKey];
state.decisionApproachDetailsById?.[target.overrideKey] ?? const value = saved
decisionApproachPresetFor(target.overrideKey); ? {
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
}
: emptyDecisionApproachTextFields(
decisionApproachPresetFor(target.overrideKey),
);
return { return {
groupKey: "decisionApproaches", groupKey: "decisionApproaches",
value: { value,
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
},
}; };
} }
case "conflictManagement": { case "conflictManagement": {
const saved = const saved = state.conflictManagementDetailsById?.[target.overrideKey];
state.conflictManagementDetailsById?.[target.overrideKey] ?? const value = saved
conflictManagementPresetFor(target.overrideKey); ? {
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
}
: emptyConflictManagementDetails();
return { return {
groupKey: "conflictManagement", groupKey: "conflictManagement",
value: { value,
...saved,
applicableScope: [...saved.applicableScope],
selectedApplicableScope: [...saved.selectedApplicableScope],
},
}; };
} }
} }
@@ -1,10 +1,10 @@
"use client"; "use client";
/** /**
* Shared "labelled text area" field used by every create flow modal section. * Shared labelled text area for create-flow modal sections.
* Pairs an `InputLabel` with a `TextArea` set to the embedded appearance — * Figma: "Step Modal" Text Area Section (23746:49780) when `generateDraftText`
* matching the Figma "Control / Text Area" pattern. Section help marks stay * is set — tall compose field + Generate draft control. Otherwise the
* off until tooltip behavior ships. * embedded Control / Text Area pattern.
*/ */
import { memo, useId } from "react"; import { memo, useId } from "react";
@@ -26,9 +26,40 @@ export interface ModalTextAreaFieldProps {
placeholder?: string; placeholder?: string;
/** Disable the field. */ /** Disable the field. */
disabled?: boolean; disabled?: boolean;
/**
* Catalog/preset copy inserted by Generate draft. When a non-empty string
* is passed, the Figma compose field + ghost control are shown.
*/
generateDraftText?: string;
/** Visible label for the Generate draft control. */
generateDraftLabel?: string;
/** Hint beside Generate draft. */
generateDraftHint?: string;
className?: string; className?: string;
} }
function PlusGlyph() {
return (
<svg
aria-hidden
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0 text-[var(--color-content-default-brand-primary,#fefcc9)]"
>
<path
d="M7 3V11M3 7H11"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function ModalTextAreaFieldComponent({ function ModalTextAreaFieldComponent({
label, label,
helpIcon = false, helpIcon = false,
@@ -37,12 +68,21 @@ function ModalTextAreaFieldComponent({
rows = 4, rows = 4,
placeholder, placeholder,
disabled = false, disabled = false,
generateDraftText,
generateDraftLabel,
generateDraftHint,
className = "", className = "",
}: ModalTextAreaFieldProps) { }: ModalTextAreaFieldProps) {
const labelId = useId(); const labelId = useId();
const draftSource = generateDraftText?.trim() ?? "";
const showGenerateDraft =
draftSource.length > 0 && Boolean(generateDraftLabel);
const compose = showGenerateDraft;
return ( return (
<div className={`flex flex-col gap-2 ${className}`.trim()}> <div
className={`flex flex-col ${compose ? "gap-[12px]" : "gap-2"} ${className}`.trim()}
>
<div id={labelId}> <div id={labelId}>
<InputLabel <InputLabel
label={label} label={label}
@@ -56,12 +96,29 @@ function ModalTextAreaFieldComponent({
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
size="large" size="large"
rows={rows} rows={compose ? 12 : rows}
appearance="embedded" appearance={compose ? "compose" : "embedded"}
placeholder={placeholder} placeholder={placeholder}
disabled={disabled} disabled={disabled}
aria-labelledby={labelId} aria-labelledby={labelId}
/> />
{showGenerateDraft && !disabled ? (
<div className="flex items-center">
<button
type="button"
onClick={() => onChange(draftSource)}
className="flex shrink-0 items-center justify-center gap-[var(--measures-spacing-050,2px)] overflow-clip rounded-[var(--measures-radius-full,9999px)] px-[var(--space-250,10px)] py-[var(--measures-spacing-200,8px)] text-x-small-label text-[color:var(--color-content-default-primary,white)] hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-default-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-transparent"
>
<PlusGlyph />
{generateDraftLabel}
</button>
{generateDraftHint ? (
<p className="text-[length:var(--sizing-300,12px)] leading-normal text-[color:var(--color-content-default-tertiary,#b4b4b4)]">
{generateDraftHint}
</p>
) : null}
</div>
) : null}
</div> </div>
); );
} }
@@ -17,6 +17,8 @@ export interface CommunicationMethodEditFieldsProps {
onChange: (_next: CommunicationMethodDetailEntry) => void; onChange: (_next: CommunicationMethodDetailEntry) => void;
/** When true, fields are not editable (view mode). */ /** When true, fields are not editable (view mode). */
readOnly?: boolean; readOnly?: boolean;
/** Catalog copy inserted by Generate draft. */
generateDraft?: CommunicationMethodDetailEntry;
} }
const FIELDS: ReadonlyArray<keyof CommunicationMethodDetailEntry> = [ const FIELDS: ReadonlyArray<keyof CommunicationMethodDetailEntry> = [
@@ -29,9 +31,11 @@ function CommunicationMethodEditFieldsComponent({
value, value,
onChange, onChange,
readOnly = false, readOnly = false,
generateDraft,
}: CommunicationMethodEditFieldsProps) { }: CommunicationMethodEditFieldsProps) {
const m = useMessages(); const m = useMessages();
const t = m.create.customRule.communication; const t = m.create.customRule.communication;
const draftField = m.create.draftField;
const patch = useCallback( const patch = useCallback(
<K extends keyof CommunicationMethodDetailEntry>( <K extends keyof CommunicationMethodDetailEntry>(
@@ -53,6 +57,10 @@ function CommunicationMethodEditFieldsComponent({
value={value[field]} value={value[field]}
onChange={(v) => patch(field, v)} onChange={(v) => patch(field, v)}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.[field]}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
))} ))}
</div> </div>
@@ -38,15 +38,19 @@ export interface ConflictManagementEditFieldsProps {
value: ConflictManagementDetailEntry; value: ConflictManagementDetailEntry;
onChange: (_next: ConflictManagementDetailEntry) => void; onChange: (_next: ConflictManagementDetailEntry) => void;
readOnly?: boolean; readOnly?: boolean;
/** Catalog copy inserted by Generate draft. */
generateDraft?: ConflictManagementDetailEntry;
} }
function ConflictManagementEditFieldsComponent({ function ConflictManagementEditFieldsComponent({
value, value,
onChange, onChange,
readOnly = false, readOnly = false,
generateDraft,
}: ConflictManagementEditFieldsProps) { }: ConflictManagementEditFieldsProps) {
const m = useMessages(); const m = useMessages();
const t = m.create.customRule.conflictManagement; const t = m.create.customRule.conflictManagement;
const draftField = m.create.draftField;
const patch = useCallback( const patch = useCallback(
<K extends keyof ConflictManagementDetailEntry>( <K extends keyof ConflictManagementDetailEntry>(
@@ -65,6 +69,10 @@ function ConflictManagementEditFieldsComponent({
value={value.corePrinciple} value={value.corePrinciple}
onChange={(v) => patch("corePrinciple", v)} onChange={(v) => patch("corePrinciple", v)}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.corePrinciple}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
<ModalTextAreaField <ModalTextAreaField
label={t.sectionHeadings.applicableScope} label={t.sectionHeadings.applicableScope}
@@ -73,18 +81,35 @@ function ConflictManagementEditFieldsComponent({
onChange={(v) => onChange(conflictDetailWithScopeTextarea(value, v))} onChange={(v) => onChange(conflictDetailWithScopeTextarea(value, v))}
rows={4} rows={4}
disabled={readOnly} disabled={readOnly}
generateDraftText={
generateDraft && generateDraft.applicableScope.length > 0
? generateDraft.applicableScope.join("\n")
: generateDraft
? conflictScopeTextareaValue(generateDraft)
: undefined
}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
<ModalTextAreaField <ModalTextAreaField
label={t.sectionHeadings.processProtocol} label={t.sectionHeadings.processProtocol}
value={value.processProtocol} value={value.processProtocol}
onChange={(v) => patch("processProtocol", v)} onChange={(v) => patch("processProtocol", v)}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.processProtocol}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
<ModalTextAreaField <ModalTextAreaField
label={t.sectionHeadings.restorationFallbacks} label={t.sectionHeadings.restorationFallbacks}
value={value.restorationFallbacks} value={value.restorationFallbacks}
onChange={(v) => patch("restorationFallbacks", v)} onChange={(v) => patch("restorationFallbacks", v)}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.restorationFallbacks}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
</div> </div>
); );
@@ -17,15 +17,19 @@ export interface CoreValueEditFieldsProps {
onChange: (_next: CoreValueDetailEntry) => void; onChange: (_next: CoreValueDetailEntry) => void;
/** Disable meaning/signals. Facet and final-review callers omit this so fields stay editable on open. */ /** Disable meaning/signals. Facet and final-review callers omit this so fields stay editable on open. */
readOnly?: boolean; readOnly?: boolean;
/** Catalog copy inserted by Generate draft. */
generateDraft?: CoreValueDetailEntry;
} }
function CoreValueEditFieldsComponent({ function CoreValueEditFieldsComponent({
value, value,
onChange, onChange,
readOnly = false, readOnly = false,
generateDraft,
}: CoreValueEditFieldsProps) { }: CoreValueEditFieldsProps) {
const m = useMessages(); const m = useMessages();
const t = m.create.customRule.coreValues.detailModal; const t = m.create.customRule.coreValues.detailModal;
const draftField = m.create.draftField;
const patch = useCallback( const patch = useCallback(
<K extends keyof CoreValueDetailEntry>( <K extends keyof CoreValueDetailEntry>(
@@ -45,6 +49,10 @@ function CoreValueEditFieldsComponent({
onChange={(v) => patch("meaning", v)} onChange={(v) => patch("meaning", v)}
rows={4} rows={4}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.meaning}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
<ModalTextAreaField <ModalTextAreaField
label={t.signalsLabel} label={t.signalsLabel}
@@ -52,6 +60,10 @@ function CoreValueEditFieldsComponent({
onChange={(v) => patch("signals", v)} onChange={(v) => patch("signals", v)}
rows={4} rows={4}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.signals}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
</div> </div>
); );
@@ -19,6 +19,8 @@ export interface DecisionApproachEditFieldsProps {
value: DecisionApproachDetailEntry; value: DecisionApproachDetailEntry;
onChange: (_next: DecisionApproachDetailEntry) => void; onChange: (_next: DecisionApproachDetailEntry) => void;
readOnly?: boolean; readOnly?: boolean;
/** Catalog copy inserted by Generate draft on text fields. */
generateDraft?: DecisionApproachDetailEntry;
} }
const CONSENSUS_LEVEL_MIN = 0; const CONSENSUS_LEVEL_MIN = 0;
@@ -29,9 +31,11 @@ function DecisionApproachEditFieldsComponent({
value, value,
onChange, onChange,
readOnly = false, readOnly = false,
generateDraft,
}: DecisionApproachEditFieldsProps) { }: DecisionApproachEditFieldsProps) {
const m = useMessages(); const m = useMessages();
const t = m.create.customRule.decisionApproaches; const t = m.create.customRule.decisionApproaches;
const draftField = m.create.draftField;
const scopes = useMemo( const scopes = useMemo(
() => withDecisionApproachKeyResourceScopes(value.applicableScope), () => withDecisionApproachKeyResourceScopes(value.applicableScope),
@@ -55,6 +59,10 @@ function DecisionApproachEditFieldsComponent({
value={value.corePrinciple} value={value.corePrinciple}
onChange={(v) => patch("corePrinciple", v)} onChange={(v) => patch("corePrinciple", v)}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.corePrinciple}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
<ApplicableScopeField <ApplicableScopeField
label={t.sectionHeadings.applicableScope} label={t.sectionHeadings.applicableScope}
@@ -79,6 +87,10 @@ function DecisionApproachEditFieldsComponent({
value={value.stepByStepInstructions} value={value.stepByStepInstructions}
onChange={(v) => patch("stepByStepInstructions", v)} onChange={(v) => patch("stepByStepInstructions", v)}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.stepByStepInstructions}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
{value.consensusLevel !== undefined ? ( {value.consensusLevel !== undefined ? (
<IncrementerBlock <IncrementerBlock
@@ -100,6 +112,10 @@ function DecisionApproachEditFieldsComponent({
value={value.objectionsDeadlocks} value={value.objectionsDeadlocks}
onChange={(v) => patch("objectionsDeadlocks", v)} onChange={(v) => patch("objectionsDeadlocks", v)}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.objectionsDeadlocks}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
</div> </div>
); );
@@ -16,6 +16,8 @@ export interface MembershipMethodEditFieldsProps {
value: MembershipMethodDetailEntry; value: MembershipMethodDetailEntry;
onChange: (_next: MembershipMethodDetailEntry) => void; onChange: (_next: MembershipMethodDetailEntry) => void;
readOnly?: boolean; readOnly?: boolean;
/** Catalog copy inserted by Generate draft. */
generateDraft?: MembershipMethodDetailEntry;
} }
const FIELDS: ReadonlyArray<keyof MembershipMethodDetailEntry> = [ const FIELDS: ReadonlyArray<keyof MembershipMethodDetailEntry> = [
@@ -28,9 +30,11 @@ function MembershipMethodEditFieldsComponent({
value, value,
onChange, onChange,
readOnly = false, readOnly = false,
generateDraft,
}: MembershipMethodEditFieldsProps) { }: MembershipMethodEditFieldsProps) {
const m = useMessages(); const m = useMessages();
const t = m.create.customRule.membership; const t = m.create.customRule.membership;
const draftField = m.create.draftField;
const patch = useCallback( const patch = useCallback(
<K extends keyof MembershipMethodDetailEntry>( <K extends keyof MembershipMethodDetailEntry>(
@@ -52,6 +56,10 @@ function MembershipMethodEditFieldsComponent({
value={value[field]} value={value[field]}
onChange={(v) => patch(field, v)} onChange={(v) => patch(field, v)}
disabled={readOnly} disabled={readOnly}
placeholder={draftField.placeholder}
generateDraftText={generateDraft?.[field]}
generateDraftLabel={draftField.generateDraft}
generateDraftHint={draftField.generateDraftHint}
/> />
))} ))}
</div> </div>
@@ -29,7 +29,7 @@ import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { CommunicationMethodEditFields } from "../../components/methodEditFields"; import { CommunicationMethodEditFields } from "../../components/methodEditFields";
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard"; import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer"; import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
import { communicationPresetFor } from "../../../../../lib/create/finalReviewChipPresets"; import { communicationPresetFor, emptyCommunicationDetails } from "../../../../../lib/create/finalReviewChipPresets";
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks"; import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom"; import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder"; import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
@@ -146,7 +146,7 @@ export function CommunicationMethodsScreen() {
if (saved) { if (saved) {
return { ...saved }; return { ...saved };
} }
return communicationPresetFor(id); return emptyCommunicationDetails();
}, },
[state.communicationMethodDetailsById], [state.communicationMethodDetailsById],
); );
@@ -756,6 +756,7 @@ export function CommunicationMethodsScreen() {
<CommunicationMethodEditFields <CommunicationMethodEditFields
value={pendingDraft} value={pendingDraft}
onChange={handleDraftChange} onChange={handleDraftChange}
generateDraft={communicationPresetFor(pendingCardId)}
/> />
) )
) : null} ) : null}
@@ -26,7 +26,7 @@ import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { ConflictManagementEditFields } from "../../components/methodEditFields"; import { ConflictManagementEditFields } from "../../components/methodEditFields";
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard"; import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer"; import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
import { conflictManagementPresetFor } from "../../../../../lib/create/finalReviewChipPresets"; import { conflictManagementPresetFor, emptyConflictManagementDetails } from "../../../../../lib/create/finalReviewChipPresets";
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks"; import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom"; import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder"; import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
@@ -147,7 +147,7 @@ export function ConflictManagementScreen() {
selectedApplicableScope: [...saved.selectedApplicableScope], selectedApplicableScope: [...saved.selectedApplicableScope],
}; };
} }
return conflictManagementPresetFor(id); return emptyConflictManagementDetails();
}, },
[state.conflictManagementDetailsById], [state.conflictManagementDetailsById],
); );
@@ -757,6 +757,7 @@ export function ConflictManagementScreen() {
<ConflictManagementEditFields <ConflictManagementEditFields
value={pendingDraft} value={pendingDraft}
onChange={handleDraftChange} onChange={handleDraftChange}
generateDraft={conflictManagementPresetFor(pendingCardId)}
/> />
) )
) : null} ) : null}
@@ -27,7 +27,7 @@ import InlineTextButton from "../../../../components/buttons/InlineTextButton";
import { MembershipMethodEditFields } from "../../components/methodEditFields"; import { MembershipMethodEditFields } from "../../components/methodEditFields";
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard"; import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer"; import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
import { membershipPresetFor } from "../../../../../lib/create/finalReviewChipPresets"; import { membershipPresetFor, emptyMembershipDetails } from "../../../../../lib/create/finalReviewChipPresets";
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks"; import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom"; import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder"; import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
@@ -144,7 +144,7 @@ export function MembershipMethodsScreen() {
if (saved) { if (saved) {
return { ...saved }; return { ...saved };
} }
return membershipPresetFor(id); return emptyMembershipDetails();
}, },
[state.membershipMethodDetailsById], [state.membershipMethodDetailsById],
); );
@@ -750,6 +750,7 @@ export function MembershipMethodsScreen() {
<MembershipMethodEditFields <MembershipMethodEditFields
value={pendingDraft} value={pendingDraft}
onChange={handleDraftChange} onChange={handleDraftChange}
generateDraft={membershipPresetFor(pendingCardId)}
/> />
) )
) : null} ) : null}
@@ -33,7 +33,7 @@ import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoCo
import { DecisionApproachEditFields } from "../../components/methodEditFields"; import { DecisionApproachEditFields } from "../../components/methodEditFields";
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard"; import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer"; import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
import { decisionApproachPresetFor } from "../../../../../lib/create/finalReviewChipPresets"; import { decisionApproachPresetFor, emptyDecisionApproachTextFields } from "../../../../../lib/create/finalReviewChipPresets";
import { import {
applyDecisionApproachKeyResources, applyDecisionApproachKeyResources,
decisionApproachKeyResourceCheckboxIds, decisionApproachKeyResourceCheckboxIds,
@@ -186,7 +186,7 @@ export function DecisionApproachesScreen() {
if (saved) { if (saved) {
return structuredClone(saved); return structuredClone(saved);
} }
return decisionApproachPresetFor(id); return emptyDecisionApproachTextFields(decisionApproachPresetFor(id));
}, },
[state.decisionApproachDetailsById], [state.decisionApproachDetailsById],
); );
@@ -840,6 +840,7 @@ export function DecisionApproachesScreen() {
<DecisionApproachEditFields <DecisionApproachEditFields
value={pendingDraft} value={pendingDraft}
onChange={handleDraftChange} onChange={handleDraftChange}
generateDraft={decisionApproachPresetFor(pendingCardId)}
/> />
) )
) : null} ) : null}
@@ -183,16 +183,16 @@ export function CoreValuesSelectScreen() {
); );
const getInitialTexts = useCallback( const getInitialTexts = useCallback(
(chipId: string, valueLabel: string): CoreValueDetailEntry => { (chipId: string, _valueLabel: string): CoreValueDetailEntry => {
const saved = state.coreValueDetailsByChipId?.[chipId]; const saved = state.coreValueDetailsByChipId?.[chipId];
const preset = getPresetTexts(valueLabel); if (!saved) return EMPTY_DETAIL;
return { return {
meaning: saved?.meaning ?? preset.meaning, meaning: saved.meaning,
signals: saved?.signals ?? preset.signals, signals: saved.signals,
...(saved?.supportText ? { supportText: saved.supportText } : {}), ...(saved.supportText ? { supportText: saved.supportText } : {}),
}; };
}, },
[state.coreValueDetailsByChipId, getPresetTexts], [state.coreValueDetailsByChipId],
); );
const openModal = useCallback( const openModal = useCallback(
@@ -799,6 +799,7 @@ export function CoreValuesSelectScreen() {
<CoreValueEditFields <CoreValueEditFields
value={draft} value={draft}
onChange={handleDraftChange} onChange={handleDraftChange}
generateDraft={getPresetTexts(modalChipLabel)}
/> />
)} )}
</Create> </Create>
@@ -93,7 +93,7 @@ const ChipContainer = memo<ChipProps>(
onInputChange={isCustom ? setInputValue : undefined} onInputChange={isCustom ? setInputValue : undefined}
onInputKeyDown={isCustom ? handleKeyDown : undefined} onInputKeyDown={isCustom ? handleKeyDown : undefined}
inputRef={isCustom ? inputRef : undefined} inputRef={isCustom ? inputRef : undefined}
ariaLabel={ariaLabel} ariaLabel={ariaLabel ?? (isCustom ? undefined : label)}
confirmAriaLabel={t("chipConfirm")} confirmAriaLabel={t("chipConfirm")}
typeToAddPlaceholder={t("chipTypeToAdd")} typeToAddPlaceholder={t("chipTypeToAdd")}
closeAriaLabel={t("chipClose")} closeAriaLabel={t("chipClose")}
+1 -1
View File
@@ -156,7 +156,7 @@ function ChipView({
}; };
const sharedA11y = { const sharedA11y = {
"aria-label": ariaLabel, ...(ariaLabel ? { "aria-label": ariaLabel } : {}),
...(isToggle ? { "aria-pressed": isSelected } : {}), ...(isToggle ? { "aria-pressed": isSelected } : {}),
}; };
@@ -46,6 +46,8 @@ const MultiSelectContainer = memo<MultiSelectProps>(
addButtonAriaLabel={ addButtonAriaLabel={
addButtonText || t("multiSelectAddFallback") addButtonText || t("multiSelectAddFallback")
} }
chipSelectAriaLabel={t("chipSelect")}
chipDeselectAriaLabel={t("chipDeselect")}
formHeader={formHeader} formHeader={formHeader}
onCustomChipConfirm={onCustomChipConfirm} onCustomChipConfirm={onCustomChipConfirm}
onCustomChipClose={onCustomChipClose} onCustomChipClose={onCustomChipClose}
@@ -85,6 +85,8 @@ export interface MultiSelectViewProps {
addButton: boolean; addButton: boolean;
addButtonText?: string; addButtonText?: string;
addButtonAriaLabel: string; addButtonAriaLabel: string;
chipSelectAriaLabel: string;
chipDeselectAriaLabel: string;
formHeader: boolean; formHeader: boolean;
onCustomChipConfirm?: (chipId: string, value: string) => void; onCustomChipConfirm?: (chipId: string, value: string) => void;
onCustomChipClose?: (chipId: string) => void; onCustomChipClose?: (chipId: string) => void;
@@ -16,6 +16,8 @@ function MultiSelectView({
addButton, addButton,
addButtonText, addButtonText,
addButtonAriaLabel, addButtonAriaLabel,
chipSelectAriaLabel,
chipDeselectAriaLabel,
formHeader = true, formHeader = true,
onCustomChipConfirm, onCustomChipConfirm,
onCustomChipClose, onCustomChipClose,
@@ -88,6 +90,14 @@ function MultiSelectView({
palette={palette} palette={palette}
size={chipSize} size={chipSize}
disabled={chipDisabled} disabled={chipDisabled}
ariaLabel={
isCustom || !option.label
? undefined
: (isSelected
? chipDeselectAriaLabel
: chipSelectAriaLabel
).replace("{label}", option.label)
}
onClick={() => { onClick={() => {
if (!isCustom && !chipDisabled && onChipClick) { if (!isCustom && !chipDisabled && onChipClick) {
onChipClick(option.id); onChipClick(option.id);
@@ -86,6 +86,21 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
textarea: string; textarea: string;
label: string; label: string;
} => { } => {
if (appearance === "compose") {
if (disabled) {
return {
textarea:
"border-0 bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-tertiary,#b4b4b4)] cursor-not-allowed opacity-60 min-h-[356px] h-[356px] px-[var(--measures-spacing-300,12px)] py-[var(--space-250,10px)]",
label: "text-[var(--color-content-default-secondary)]",
};
}
return {
textarea:
"border-0 bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] placeholder:text-[var(--color-content-default-tertiary,#b4b4b4)] min-h-[356px] h-[356px] px-[var(--measures-spacing-300,12px)] py-[var(--space-250,10px)] focus:outline-none focus:ring-0",
label: "text-[var(--color-content-default-secondary)]",
};
}
if (appearance === "embedded") { if (appearance === "embedded") {
if (disabled) { if (disabled) {
return { return {
@@ -161,8 +176,8 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
const textareaClasses = ` const textareaClasses = `
scrollbar-design w-full transition-all duration-200 ease-in-out resize-none scrollbar-design w-full transition-all duration-200 ease-in-out resize-none
${appearance === "embedded" ? "rounded-[var(--radius-300,12px)]" : "border"} ${appearance === "embedded" || appearance === "compose" ? "rounded-[var(--radius-300,12px)]" : "border"}
${currentSize.textarea} ${appearance === "compose" ? "text-medium-paragraph" : currentSize.textarea}
${stateStyles.textarea} ${stateStyles.textarea}
${className} ${className}
`.trim(); `.trim();
@@ -1,9 +1,12 @@
import type { InputStateValue } from "../../../../lib/propNormalization"; import type {
InputStateValue,
TextAreaAppearanceValue,
} from "../../../../lib/propNormalization";
export type TextAreaSizeValue = "small" | "medium" | "large"; export type TextAreaSizeValue = "small" | "medium" | "large";
export type TextAreaLabelVariantValue = "default" | "horizontal"; export type TextAreaLabelVariantValue = "default" | "horizontal";
export type TextAreaAppearanceValue = "default" | "embedded"; export type { TextAreaAppearanceValue };
export interface TextAreaProps extends Omit< export interface TextAreaProps extends Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>, React.TextareaHTMLAttributes<HTMLTextAreaElement>,
@@ -49,7 +52,8 @@ export interface TextAreaProps extends Omit<
/** /**
* Visual appearance. "embedded" matches Create modal sections (Figma 20736-12668): * Visual appearance. "embedded" matches Create modal sections (Figma 20736-12668):
* borderless, darker grey background, tertiary text in default (primary on * borderless, darker grey background, tertiary text in default (primary on
* focus). "default" is the standard bordered input. * focus). "compose" is the tall empty creation field (Figma 23746:49780).
* "default" is the standard bordered input.
* @default "default" * @default "default"
*/ */
appearance?: TextAreaAppearanceValue; appearance?: TextAreaAppearanceValue;
@@ -79,7 +83,7 @@ export interface TextAreaViewProps {
textHint?: boolean | string; textHint?: boolean | string;
formHeader?: boolean; formHeader?: boolean;
showHelpIcon?: boolean; showHelpIcon?: boolean;
appearance?: "default" | "embedded"; appearance?: TextAreaAppearanceValue;
helpIconAlt: string; helpIconAlt: string;
hintDefault: string; hintDefault: string;
} }
+38 -9
View File
@@ -14,11 +14,10 @@ import type {
} from "../../app/(app)/create/types"; } from "../../app/(app)/create/types";
/** /**
* Per-method preset defaults shipped in `messages/en/create/customRule/*.json`. * Catalog copy for method/value sections. Generate draft inserts these into
* Used to seed the final-review edit modal when the user has no saved * empty creation-modal fields. Publish still merges them onto saved overrides
* override yet, and to merge onto overrides when emitting the published rule * so every method carries a complete section payload when the author only
* document so every method carries a complete section payload even if the * edited a subset of fields.
* author only edited a subset of fields.
*/ */
type CustomRuleMethodRow = { type CustomRuleMethodRow = {
id: string; id: string;
@@ -96,6 +95,10 @@ export function communicationPresetFor(
}; };
} }
export function emptyCommunicationDetails(): CommunicationMethodDetailEntry {
return { corePrinciple: "", logisticsAdmin: "", codeOfConduct: "" };
}
export function membershipPresetFor(id: string): MembershipMethodDetailEntry { export function membershipPresetFor(id: string): MembershipMethodDetailEntry {
const method = findMethod(membershipMessages, id); const method = findMethod(membershipMessages, id);
const s = method?.sections ?? {}; const s = method?.sections ?? {};
@@ -106,6 +109,10 @@ export function membershipPresetFor(id: string): MembershipMethodDetailEntry {
}; };
} }
export function emptyMembershipDetails(): MembershipMethodDetailEntry {
return { eligibility: "", joiningProcess: "", expectations: "" };
}
/** Default consensus level used when a **catalog** preset omits a value. */ /** Default consensus level used when a **catalog** preset omits a value. */
export const DECISION_CONSENSUS_LEVEL_DEFAULT = 75; export const DECISION_CONSENSUS_LEVEL_DEFAULT = 75;
@@ -132,6 +139,20 @@ export function decisionApproachPresetFor(
return entry; return entry;
} }
/** Catalog scopes/consensus kept; text fields start empty for Generate draft. */
export function emptyDecisionApproachTextFields(
preset: DecisionApproachDetailEntry,
): DecisionApproachDetailEntry {
return {
...preset,
corePrinciple: "",
stepByStepInstructions: "",
objectionsDeadlocks: "",
applicableScope: [...preset.applicableScope],
selectedApplicableScope: [...preset.selectedApplicableScope],
};
}
export function conflictManagementPresetFor( export function conflictManagementPresetFor(
id: string, id: string,
): ConflictManagementDetailEntry { ): ConflictManagementDetailEntry {
@@ -146,11 +167,19 @@ export function conflictManagementPresetFor(
}; };
} }
export function emptyConflictManagementDetails(): ConflictManagementDetailEntry {
return {
corePrinciple: "",
applicableScope: [],
selectedApplicableScope: [],
processProtocol: "",
restorationFallbacks: "",
};
}
/** /**
* Preset meaning/signals for a core value chip. Mirrors `CoreValuesSelectScreen`'s * Preset meaning/signals for a core value chip. Used by Generate draft and
* `getInitialTexts` so the final-review edit modal opens with the same * by publish/display merge when the author has not saved an override.
* preset copy the select screen would have shown — without requiring the
* user to have opened the select-screen modal first.
* *
* Lookup order: * Lookup order:
* 1. Numeric chip id → 1-based index into `coreValues.json` `values[]` * 1. Numeric chip id → 1-based index into `coreValues.json` `values[]`
+7 -45
View File
@@ -89,46 +89,6 @@ function clampPercent(n: number): number {
return Math.min(100, Math.max(1, Math.round(n))); return Math.min(100, Math.max(1, Math.round(n)));
} }
function facetPrefillHasContent(prefill: MethodCardWizardFacetPrefill): boolean {
switch (prefill.group) {
case "communication":
return (
hasTrimmedText(prefill.draft.corePrinciple) ||
hasTrimmedText(prefill.draft.logisticsAdmin) ||
hasTrimmedText(prefill.draft.codeOfConduct)
);
case "membership":
return (
hasTrimmedText(prefill.draft.eligibility) ||
hasTrimmedText(prefill.draft.joiningProcess) ||
hasTrimmedText(prefill.draft.expectations)
);
case "decisionApproaches":
return (
hasTrimmedText(prefill.draft.corePrinciple) ||
hasTrimmedText(prefill.draft.stepByStepInstructions) ||
hasTrimmedText(prefill.draft.objectionsDeadlocks) ||
prefill.draft.applicableScope.length > 0 ||
prefill.draft.selectedApplicableScope.length > 0
);
case "conflictManagement":
return (
hasTrimmedText(prefill.draft.corePrinciple) ||
hasTrimmedText(prefill.draft.processProtocol) ||
hasTrimmedText(prefill.draft.restorationFallbacks) ||
formatConflictApplicableScopeForTextarea(
prefill.draft.selectedApplicableScope,
prefill.draft.applicableScope,
).trim().length > 0
);
case "coreValues":
return (
hasTrimmedText(prefill.draft.meaning) ||
hasTrimmedText(prefill.draft.signals)
);
}
}
function mapFacetPrefillToWizardFieldBlocks( function mapFacetPrefillToWizardFieldBlocks(
prefill: MethodCardWizardFacetPrefill, prefill: MethodCardWizardFacetPrefill,
): CustomMethodCardFieldBlock[] { ): CustomMethodCardFieldBlock[] {
@@ -198,7 +158,11 @@ function mapFacetPrefillToWizardFieldBlocks(
); );
if ( if (
typeof prefill.draft.consensusLevel === "number" && typeof prefill.draft.consensusLevel === "number" &&
facetPrefillHasContent(prefill) (hasTrimmedText(prefill.draft.corePrinciple) ||
hasTrimmedText(prefill.draft.stepByStepInstructions) ||
hasTrimmedText(prefill.draft.objectionsDeadlocks) ||
prefill.draft.applicableScope.length > 0 ||
prefill.draft.selectedApplicableScope.length > 0)
) { ) {
blocks.push({ blocks.push({
kind: "proportion", kind: "proportion",
@@ -260,14 +224,12 @@ function mapFacetPrefillToWizardFieldBlocks(
/** /**
* Map a method card's facet editors onto wizard field blocks so Customize * Map a method card's facet editors onto wizard field blocks so Customize
* can show and edit the same sections on step 3. * can show and edit the same sections on step 3. Empty copy still yields
* the catalog headings so authors can fill them in the wizard.
*/ */
export function facetDetailsToWizardFieldBlocks( export function facetDetailsToWizardFieldBlocks(
prefill: MethodCardWizardFacetPrefill, prefill: MethodCardWizardFacetPrefill,
): CustomMethodCardFieldBlock[] { ): CustomMethodCardFieldBlock[] {
if (!facetPrefillHasContent(prefill)) {
return [];
}
return mapFacetPrefillToWizardFieldBlocks(prefill); return mapFacetPrefillToWizardFieldBlocks(prefill);
} }
+5 -1
View File
@@ -178,7 +178,11 @@ export type ToggleGroupPositionValue =
export const LABEL_VARIANT_OPTIONS = ["default", "horizontal"] as const; export const LABEL_VARIANT_OPTIONS = ["default", "horizontal"] as const;
export type LabelVariantValue = (typeof LABEL_VARIANT_OPTIONS)[number]; export type LabelVariantValue = (typeof LABEL_VARIANT_OPTIONS)[number];
export const TEXT_AREA_APPEARANCE_OPTIONS = ["default", "embedded"] as const; export const TEXT_AREA_APPEARANCE_OPTIONS = [
"default",
"embedded",
"compose",
] as const;
export type TextAreaAppearanceValue = export type TextAreaAppearanceValue =
(typeof TEXT_AREA_APPEARANCE_OPTIONS)[number]; (typeof TEXT_AREA_APPEARANCE_OPTIONS)[number];
@@ -5,6 +5,8 @@
"chipConfirm": "Confirm", "chipConfirm": "Confirm",
"chipTypeToAdd": "Type to add", "chipTypeToAdd": "Type to add",
"chipClose": "Close", "chipClose": "Close",
"chipSelect": "Select {label}",
"chipDeselect": "Deselect {label}",
"selectPlaceholder": "Choose an option", "selectPlaceholder": "Choose an option",
"selectAriaLabel": "Select an option", "selectAriaLabel": "Select an option",
"multiSelectAddDefault": "Add organization type", "multiSelectAddDefault": "Add organization type",
+6
View File
@@ -0,0 +1,6 @@
{
"_comment": "Generate-draft control on creation-modal text fields (Figma Step Modal 23746:49780).",
"generateDraft": "Generate draft",
"generateDraftHint": "Get a first draft for this section",
"placeholder": "Describe how this method should work"
}
+2
View File
@@ -69,6 +69,7 @@ import createTopNav from "./create/topNav.json";
import createDraftHydration from "./create/draftHydration.json"; import createDraftHydration from "./create/draftHydration.json";
import createTemplateReview from "./create/templateReview.json"; import createTemplateReview from "./create/templateReview.json";
import createUpload from "./create/upload.json"; import createUpload from "./create/upload.json";
import createDraftField from "./create/draftField.json";
export default { export default {
common, common,
@@ -138,6 +139,7 @@ export default {
draftHydration: createDraftHydration, draftHydration: createDraftHydration,
templateReview: createTemplateReview, templateReview: createTemplateReview,
upload: createUpload, upload: createUpload,
draftField: createDraftField,
}, },
navigation, navigation,
metadata, metadata,
@@ -9,7 +9,7 @@ export default {
docs: { docs: {
description: { description: {
component: component:
"Shared 'labelled text area' field used by every create-flow modal section. Pairs `InputLabel` with a `TextArea` set to the `embedded` appearance. Section help marks stay off until tooltip behavior ships.", "Shared labelled text area for create-flow modal sections. With `generateDraftText`, matches Figma Step Modal (23746:49780): tall compose field plus Generate draft.",
}, },
}, },
}, },
@@ -58,6 +58,25 @@ export const WithValue = {
}, },
}; };
export const GenerateDraft = {
render: () => {
const [value, setValue] = React.useState("");
return (
<div className="w-[520px]">
<ModalTextAreaField
label="What does this value mean to your group?"
value={value}
onChange={setValue}
placeholder="Describe how this method should work"
generateDraftText="We design spaces and tools that everyone can use regardless of ability or background."
generateDraftLabel="Generate draft"
generateDraftHint="Get a first draft for this section"
/>
</div>
);
},
};
export const Disabled = { export const Disabled = {
render: () => ( render: () => (
<div className="w-[520px]"> <div className="w-[520px]">
+7
View File
@@ -34,6 +34,13 @@ const config: ComponentTestSuiteConfig<Props> = {
describe("Chip", () => { describe("Chip", () => {
componentTestSuite<Props>(config); componentTestSuite<Props>(config);
it("uses the visible label as the accessible name when ariaLabel is omitted", () => {
render(<Chip label="Worker cooperative" state="unselected" />);
expect(
screen.getByRole("button", { name: "Worker cooperative" }),
).toHaveAttribute("aria-label", "Worker cooperative");
});
it("exposes aria-pressed false when unselected", () => { it("exposes aria-pressed false when unselected", () => {
render(<Chip label="Worker cooperative" state="unselected" />); render(<Chip label="Worker cooperative" state="unselected" />);
const chip = screen.getByRole("button", { name: "Worker cooperative" }); const chip = screen.getByRole("button", { name: "Worker cooperative" });
@@ -63,7 +63,7 @@ function ScreenWithStateProbe({
* and `buildPublishPayload` for the read side. * and `buildPublishPayload` for the read side.
*/ */
describe("CommunicationMethodsScreen — Add Platform persistence", () => { describe("CommunicationMethodsScreen — Add Platform persistence", () => {
it("seeds the modal from preset and persists edits + selection on Confirm", async () => { it("starts empty and Generate draft inserts the catalog preset", async () => {
let latest: CreateFlowState = {}; let latest: CreateFlowState = {};
render( render(
<ScreenWithStateProbe <ScreenWithStateProbe
@@ -82,9 +82,16 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
const textboxes = within(dialog).getAllByRole("textbox"); const textboxes = within(dialog).getAllByRole("textbox");
expect(textboxes.length).toBe(3); expect(textboxes.length).toBe(3);
const corePrincipleField = textboxes[0] as HTMLTextAreaElement; const corePrincipleField = textboxes[0] as HTMLTextAreaElement;
// Preset corePrinciple must seed into the first body textarea so the user expect(corePrincipleField.value).toBe("");
// edits a real starting point rather than an empty field.
expect(corePrincipleField.value.length).toBeGreaterThan(0); const generateButtons = within(dialog).getAllByRole("button", {
name: "Generate draft",
});
expect(generateButtons.length).toBe(3);
fireEvent.click(generateButtons[0]);
expect(corePrincipleField.value).toBe(
communicationPresetFor("signal").corePrinciple,
);
fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } }); fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } });
fireEvent.click( fireEvent.click(
@@ -466,6 +473,11 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0], screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
for (const button of within(dialog).getAllByRole("button", {
name: "Generate draft",
})) {
fireEvent.click(button);
}
fireEvent.click(within(dialog).getByRole("button", { name: "More options" })); fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
fireEvent.click(screen.getByRole("menuitem", { name: "Customize" })); fireEvent.click(screen.getByRole("menuitem", { name: "Customize" }));
@@ -16,4 +16,18 @@ describe("CommunityStructureSelectScreen", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.queryByAltText("Help")).not.toBeInTheDocument(); expect(screen.queryByAltText("Help")).not.toBeInTheDocument();
}); });
it("gives structure chips Select/Deselect accessible names", () => {
renderWithProviders(<CommunityStructureSelectScreen />);
expect(
screen.getByRole("button", { name: "Select Worker cooperative" }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Select Local" }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Select Early stage" }),
).toBeInTheDocument();
});
}); });
@@ -41,6 +41,26 @@ describe("CoreValuesSelectScreen", () => {
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it("gives value chips Select/Deselect accessible names", () => {
renderWithProviders(<CoreValuesSelectScreen />);
expect(
screen.getByRole("button", { name: "Select Accessibility" }),
).toBeInTheDocument();
});
it("starts the value modal empty and Generate draft inserts catalog copy", async () => {
renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByRole("button", { name: "Select Accessibility" }));
const dialog = await screen.findByRole("dialog");
const fields = within(dialog).getAllByRole("textbox") as HTMLTextAreaElement[];
expect(fields[0].value).toBe("");
expect(fields[1].value).toBe("");
fireEvent.click(
within(dialog).getAllByRole("button", { name: "Generate draft" })[0],
);
expect(fields[0].value).toMatch(/everyone can use/i);
});
it("closes a pending value without confirm when fields are unchanged", async () => { it("closes a pending value without confirm when fields are unchanged", async () => {
renderWithProviders(<CoreValuesSelectScreen />); renderWithProviders(<CoreValuesSelectScreen />);
fireEvent.click(screen.getByText("Accessibility")); fireEvent.click(screen.getByText("Accessibility"));
@@ -289,12 +309,10 @@ describe("CoreValuesSelectScreen", () => {
*/ */
function countCustomChips(label: string) { function countCustomChips(label: string) {
return screen return screen
.queryAllByRole("button", { name: label }) .queryAllByRole("button", {
.filter( name: new RegExp(`^(Select|Deselect) ${label}$`),
(el) => })
!el.closest('[role="dialog"]') && .filter((el) => !el.closest('[role="dialog"]')).length;
(el.getAttribute("aria-label") ?? "") !== `Remove ${label}`,
).length;
} }
async function completeAddValueWizard(label: string) { async function completeAddValueWizard(label: string) {
@@ -417,7 +435,7 @@ describe("CoreValuesSelectScreen", () => {
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect( expect(
screen.getByRole("button", { name: "Anti-oppression" }), screen.getByRole("button", { name: "Select Anti-oppression" }),
).toBeDisabled(); ).toBeDisabled();
expect(screen.getByRole("button", { name: "Add value" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Add value" })).toBeDisabled();
expect(screen.getByRole("button", { name: /^add$/i })).toBeDisabled(); expect(screen.getByRole("button", { name: /^add$/i })).toBeDisabled();
+29 -25
View File
@@ -12,6 +12,10 @@ import { FinalReviewScreen } from "../../app/(app)/create/screens/review/FinalRe
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext"; import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
import type { CreateFlowState } from "../../app/(app)/create/types"; import type { CreateFlowState } from "../../app/(app)/create/types";
function catalogChipName(label: string) {
return new RegExp(`^(Select|Deselect) ${label}$`);
}
async function confirmDiscardCustomizeEdits() { async function confirmDiscardCustomizeEdits() {
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Discard" }), await screen.findByRole("button", { name: "Discard" }),
@@ -232,7 +236,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
it("opens the chip modal when a chip is clicked, matching the preset copy", async () => { it("opens the chip modal when a chip is clicked, matching the preset copy", async () => {
render(<FinalReviewWithCustomizeSelections />); render(<FinalReviewWithCustomizeSelections />);
const signalChip = await screen.findByRole("button", { name: "Signal" }); const signalChip = await screen.findByRole("button", { name: catalogChipName("Signal") });
fireEvent.click(signalChip); fireEvent.click(signalChip);
// Modal subtitle is the `supportText` from communication.json for the // Modal subtitle is the `supportText` from communication.json for the
@@ -273,7 +277,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
); );
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Video Meetings" }), await screen.findByRole("button", { name: catalogChipName("Video Meetings") }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect(within(dialog).getAllByRole("textbox")).toHaveLength(3); expect(within(dialog).getAllByRole("textbox")).toHaveLength(3);
@@ -287,7 +291,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
it("method chip modal kebab offers Customize but not Duplicate", async () => { it("method chip modal kebab offers Customize but not Duplicate", async () => {
render(<FinalReviewWithCustomizeSelections />); render(<FinalReviewWithCustomizeSelections />);
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click( fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }), within(dialog).getByRole("button", { name: /more options/i }),
@@ -317,7 +321,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
} }
render(<CoreValuesHarness />); render(<CoreValuesHarness />);
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }), await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click( fireEvent.click(
@@ -348,7 +352,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
} }
render(<CoreValuesHarness />); render(<CoreValuesHarness />);
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }), await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click( fireEvent.click(
@@ -366,7 +370,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
it("opens method chip modal editable, with Save disabled until a field changes", async () => { it("opens method chip modal editable, with Save disabled until a field changes", async () => {
render(<FinalReviewWithCustomizeSelections />); render(<FinalReviewWithCustomizeSelections />);
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const saveButton = within(dialog).getByRole("button", { name: "Save" }); const saveButton = within(dialog).getByRole("button", { name: "Save" });
@@ -400,7 +404,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
} }
render(<CoreValuesHarness />); render(<CoreValuesHarness />);
const chip = await screen.findByRole("button", { name: "Accessibility" }); const chip = await screen.findByRole("button", { name: catalogChipName("Accessibility") });
fireEvent.click(chip); fireEvent.click(chip);
await waitFor(() => { await waitFor(() => {
@@ -430,7 +434,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
render(<CoreValuesHarness />); render(<CoreValuesHarness />);
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }), await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect( expect(
@@ -468,7 +472,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
render(<UseWithoutChangesHarness />); render(<UseWithoutChangesHarness />);
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }), await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect( expect(
@@ -514,7 +518,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
); );
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Accessibility" }), await screen.findByRole("button", { name: catalogChipName("Accessibility") }),
); );
const valuesDialog = await screen.findByRole("dialog"); const valuesDialog = await screen.findByRole("dialog");
expect(within(valuesDialog).getAllByRole("textbox").length).toBeGreaterThan( expect(within(valuesDialog).getAllByRole("textbox").length).toBeGreaterThan(
@@ -542,7 +546,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
/>, />,
); );
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const methodDialog = await screen.findByRole("dialog"); const methodDialog = await screen.findByRole("dialog");
expect(within(methodDialog).getAllByRole("textbox").length).toBeGreaterThan( expect(within(methodDialog).getAllByRole("textbox").length).toBeGreaterThan(
0, 0,
@@ -567,7 +571,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
/>, />,
); );
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect( expect(
within(dialog).getByRole("button", { name: "Remove" }), within(dialog).getByRole("button", { name: "Remove" }),
@@ -595,7 +599,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
/>, />,
); );
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "Remove" })); fireEvent.click(within(dialog).getByRole("button", { name: "Remove" }));
await waitFor(() => { await waitFor(() => {
@@ -633,7 +637,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
); );
void latest; void latest;
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const saveButton = within(dialog).getByRole("button", { name: "Save" }); const saveButton = within(dialog).getByRole("button", { name: "Save" });
expect(saveButton).toBeDisabled(); expect(saveButton).toBeDisabled();
@@ -663,7 +667,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>, />,
); );
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const principleField = within(dialog).getByRole("textbox", { const principleField = within(dialog).getByRole("textbox", {
name: /core principle/i, name: /core principle/i,
@@ -691,7 +695,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>, />,
); );
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const principleField = within(dialog).getByRole("textbox", { const principleField = within(dialog).getByRole("textbox", {
name: /core principle/i, name: /core principle/i,
@@ -719,7 +723,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>, />,
); );
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect(dispatchBeforeUnload()).toBe(false); expect(dispatchBeforeUnload()).toBe(false);
const principleField = within(dialog).getByRole("textbox", { const principleField = within(dialog).getByRole("textbox", {
@@ -750,7 +754,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
); );
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Custom Comm" }), await screen.findByRole("button", { name: catalogChipName("Custom Comm") }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect( expect(
@@ -784,9 +788,9 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
); );
expect( expect(
await screen.findByRole("button", { name: "Garden IRC" }), await screen.findByRole("button", { name: catalogChipName("Garden IRC") }),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Signal" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: catalogChipName("Signal") })).toBeInTheDocument();
}); });
it("shows editable field blocks for user-authored communication chips when configured", async () => { it("shows editable field blocks for user-authored communication chips when configured", async () => {
@@ -818,7 +822,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
); );
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Custom Comm" }), await screen.findByRole("button", { name: catalogChipName("Custom Comm") }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
expect( expect(
@@ -861,7 +865,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
); );
fireEvent.click( fireEvent.click(
await screen.findByRole("button", { name: "Custom Comm" }), await screen.findByRole("button", { name: catalogChipName("Custom Comm") }),
); );
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
const notesField = within(dialog).getByRole("textbox", { name: /notes/i }); const notesField = within(dialog).getByRole("textbox", { name: /notes/i });
@@ -887,7 +891,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>, />,
); );
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click( fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }), within(dialog).getByRole("button", { name: /more options/i }),
@@ -921,7 +925,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>, />,
); );
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click( fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }), within(dialog).getByRole("button", { name: /more options/i }),
@@ -957,7 +961,7 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
/>, />,
); );
fireEvent.click(await screen.findByRole("button", { name: "Signal" })); fireEvent.click(await screen.findByRole("button", { name: catalogChipName("Signal") }));
const dialog = await screen.findByRole("dialog"); const dialog = await screen.findByRole("dialog");
fireEvent.click( fireEvent.click(
within(dialog).getByRole("button", { name: /more options/i }), within(dialog).getByRole("button", { name: /more options/i }),
@@ -110,4 +110,44 @@ describe("ModalTextAreaField behavior", () => {
expect(screen.getByAltText("Help")).toBeInTheDocument(); expect(screen.getByAltText("Help")).toBeInTheDocument();
}); });
it("starts empty and Generate draft inserts the provided catalog copy", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderWithProviders(
<ModalTextAreaField
label="Core principle"
value=""
onChange={onChange}
generateDraftText="Catalog first draft"
generateDraftLabel="Generate draft"
generateDraftHint="Get a first draft for this section"
/>,
);
expect(screen.getByRole("textbox", { name: /Core principle/i })).toHaveValue(
"",
);
expect(
screen.getByText("Get a first draft for this section"),
).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Generate draft" }));
expect(onChange).toHaveBeenCalledWith("Catalog first draft");
});
it("hides Generate draft when there is no catalog copy", () => {
renderWithProviders(
<ModalTextAreaField
label="Core principle"
value=""
onChange={() => {}}
generateDraftText=""
generateDraftLabel="Generate draft"
/>,
);
expect(
screen.queryByRole("button", { name: "Generate draft" }),
).not.toBeInTheDocument();
});
}); });
+13 -3
View File
@@ -62,6 +62,16 @@ describe("MultiSelect – behaviour specifics", () => {
expect(screen.getByText("Option 2")).toBeInTheDocument(); expect(screen.getByText("Option 2")).toBeInTheDocument();
}); });
it("names toggle chips Select/Deselect so they are not unnamed buttons", () => {
render(<MultiSelect options={defaultChipOptions} />);
expect(
screen.getByRole("button", { name: "Select Option 1" }),
).toHaveAttribute("aria-pressed", "false");
expect(
screen.getByRole("button", { name: "Deselect Option 2" }),
).toHaveAttribute("aria-pressed", "true");
});
it("calls onChipClick when a chip is clicked", async () => { it("calls onChipClick when a chip is clicked", async () => {
const handleChipClick = vi.fn(); const handleChipClick = vi.fn();
render( render(
@@ -151,11 +161,11 @@ describe("MultiSelect – behaviour specifics", () => {
expect(screen.getByText("2 of 2")).toBeInTheDocument(); expect(screen.getByText("2 of 2")).toBeInTheDocument();
expect(screen.getByText("Limit reached")).toBeInTheDocument(); expect(screen.getByText("Limit reached")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "One" })).toBeEnabled(); expect(screen.getByRole("button", { name: "Deselect One" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Three" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Select Three" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Add option" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Add option" })).toBeDisabled();
await userEvent.click(screen.getByRole("button", { name: "Three" })); await userEvent.click(screen.getByRole("button", { name: "Select Three" }));
await userEvent.click(screen.getByRole("button", { name: "Add option" })); await userEvent.click(screen.getByRole("button", { name: "Add option" }));
expect(handleChipClick).not.toHaveBeenCalled(); expect(handleChipClick).not.toHaveBeenCalled();
expect(handleAddClick).not.toHaveBeenCalled(); expect(handleAddClick).not.toHaveBeenCalled();
+10
View File
@@ -18,4 +18,14 @@ describe("CommunitySizeSelectScreen", () => {
expect(screen.getByText("1 member")).toBeInTheDocument(); expect(screen.getByText("1 member")).toBeInTheDocument();
expect(screen.getByText("2-5 members")).toBeInTheDocument(); expect(screen.getByText("2-5 members")).toBeInTheDocument();
}); });
it("gives size chips Select/Deselect accessible names", () => {
render(<CommunitySizeSelectScreen />);
expect(
screen.getByRole("button", { name: "Select 1 member" }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Select 2-5 members" }),
).toBeInTheDocument();
});
}); });
+21 -2
View File
@@ -177,7 +177,7 @@ describe("buildMethodCardWizardInitialValues", () => {
}); });
describe("facetDetailsToWizardFieldBlocks", () => { describe("facetDetailsToWizardFieldBlocks", () => {
it("returns no blocks when facet copy is empty", () => { it("keeps catalog field headings when facet copy is empty", () => {
expect( expect(
facetDetailsToWizardFieldBlocks({ facetDetailsToWizardFieldBlocks({
group: "communication", group: "communication",
@@ -188,7 +188,26 @@ describe("facetDetailsToWizardFieldBlocks", () => {
}, },
headings: commHeadings, headings: commHeadings,
}), }),
).toEqual([]); ).toEqual([
{
kind: "text",
id: "facet-corePrinciple",
blockTitle: "Core Principle & Scope",
placeholderText: " ",
},
{
kind: "text",
id: "facet-logisticsAdmin",
blockTitle: "Logistics, Admin & Norms",
placeholderText: "",
},
{
kind: "text",
id: "facet-codeOfConduct",
blockTitle: "Code of Conduct",
placeholderText: "",
},
]);
}); });
it("maps decision scope chips to a badge block", () => { it("maps decision scope chips to a badge block", () => {