Show the whole Rule on one review card, with quiet flags for missing cards and Continue once each question has one.
This commit is contained in:
1 parent
cc878e3c19
commit
6e2e1915ae
30 files changed
+571
-218
No files matched your search
@@ -90,6 +90,7 @@ import {
|
||||
isCommunityNameStepBlocked,
|
||||
isCommunitySizeStepBlocked,
|
||||
isCommunityStructureStepBlocked,
|
||||
isFinalReviewContinueBlocked,
|
||||
} from "./utils/createFlowFooterLabels";
|
||||
import {
|
||||
EMPTY_SETUP_TRAITS,
|
||||
@@ -912,7 +913,7 @@ function CreateFlowLayoutContent({
|
||||
progressBar={
|
||||
!isTemplateReviewRoute &&
|
||||
!isCreateTemplatesPickerRoute &&
|
||||
!isFinalReviewLike &&
|
||||
currentStep !== "edit-rule" &&
|
||||
reviewReturnTarget !== "edit-rule"
|
||||
}
|
||||
proportionBarProgress={proportionBarProgress}
|
||||
@@ -1146,14 +1147,22 @@ function CreateFlowLayoutContent({
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={
|
||||
isPublishing || (isFinalReviewLike && !sessionResolved)
|
||||
isPublishing ||
|
||||
(isEditRuleStep && !sessionResolved) ||
|
||||
(currentStep === "final-review" &&
|
||||
isFinalReviewContinueBlocked(state))
|
||||
}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
if (isFinalReviewLike) {
|
||||
if (isEditRuleStep) {
|
||||
void handleFinalize();
|
||||
return;
|
||||
}
|
||||
if (currentStep === "final-review") {
|
||||
if (isFinalReviewContinueBlocked(state)) return;
|
||||
goToNextStep();
|
||||
return;
|
||||
}
|
||||
markAttempted();
|
||||
if (
|
||||
isCommunityStructureStepBlocked(
|
||||
@@ -1180,12 +1189,14 @@ function CreateFlowLayoutContent({
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{isFinalReviewLike
|
||||
? isPublishing
|
||||
? messages.create.reviewAndComplete.publish
|
||||
.publishButtonPublishing
|
||||
: footer.publishCommunityRule
|
||||
: getDefaultFooterLabel(currentStep, footer)}
|
||||
{currentStep === "final-review"
|
||||
? footer.continue
|
||||
: isFinalReviewLike
|
||||
? isPublishing
|
||||
? messages.create.reviewAndComplete.publish
|
||||
.publishButtonPublishing
|
||||
: footer.publishCommunityRule
|
||||
: getDefaultFooterLabel(currentStep, footer)}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const blurActiveElement = (): void => {
|
||||
* still needs identity (direct from a template) walks name / description /
|
||||
* photo, then Back from `community-name` returns to
|
||||
* `/create/review-template/{slug}`. In-flow picks (`?fromFlow=1`) skip
|
||||
* identity and land on `confirm-stakeholders`; Back from that step returns
|
||||
* identity and land on `final-review`; Back from that step returns
|
||||
* to the template instead of `conflict-management`.
|
||||
*
|
||||
* Template review footer Back uses {@link buildTemplateReviewHref}’s
|
||||
|
||||
@@ -43,7 +43,7 @@ export type UseTemplateReviewActionsResult = {
|
||||
* `/create/community-name` with a pin so Next/Back collect only name →
|
||||
* description → photo, and drops leftover setup answers along with the
|
||||
* other identity fields. In-flow (`?fromFlow=1`, community steps already
|
||||
* done) routes to `/create/confirm-stakeholders` and keeps those answers.
|
||||
* done) routes to `/create/final-review` and keeps those answers.
|
||||
*/
|
||||
handleUseWithoutChanges: () => Promise<void>;
|
||||
};
|
||||
@@ -268,7 +268,7 @@ export function useTemplateReviewActions({
|
||||
});
|
||||
router.push(
|
||||
fromCreateWizard
|
||||
? createFlowStepPath("confirm-stakeholders")
|
||||
? createFlowStepPath("final-review")
|
||||
: createFlowStepPath("community-name"),
|
||||
);
|
||||
}, [
|
||||
|
||||
@@ -14,8 +14,11 @@ import {
|
||||
} from "../../components/CreateFlowLockupCardStepShell";
|
||||
import {
|
||||
buildFinalReviewCategoryRowsDetailed,
|
||||
type FinalReviewCategoryNames,
|
||||
type FinalReviewCategoryRowDetailed,
|
||||
} from "../../../../../lib/create/buildFinalReviewCategories";
|
||||
import { communityInitialsFromTitle } from "../../../../../lib/create/communityInitials";
|
||||
import { finalReviewChipHasWarning } from "../../../../../lib/create/finalReviewChipWarnings";
|
||||
import { applyFinalReviewChipEditPatch } from "../../../../../lib/create/applyFinalReviewChipEditPatch";
|
||||
import type {
|
||||
TemplateChipDetail,
|
||||
@@ -32,10 +35,6 @@ import { useCommunityAvatarDisplayUrl } from "../../hooks/useCommunityAvatarDisp
|
||||
import { usePreviewSurfaceColor } from "../../hooks/usePreviewSurfaceColor";
|
||||
import { useCreateFlowNavigation } from "../../hooks/useCreateFlowNavigation";
|
||||
import { createFlowStepForFacetGroup } from "../../utils/facetGroupToCreateFlowStep";
|
||||
import {
|
||||
getAssetPath,
|
||||
vectorMarkPath,
|
||||
} from "../../../../../lib/assetUtils";
|
||||
import { isPreviewSurfaceColor } from "../../../../../lib/create/previewSurfaceColor";
|
||||
import {
|
||||
persistDraftPreviewSurfaceColor,
|
||||
@@ -51,53 +50,17 @@ function bindingArbitrationNeedsSeparateYes(state: CreateFlowState): boolean {
|
||||
return joinsWithoutAYes && conflict.includes("binding-arbitration");
|
||||
}
|
||||
|
||||
const FACET_FALLBACK_ORDER: readonly TemplateFacetGroupKey[] = [
|
||||
"coreValues",
|
||||
"communication",
|
||||
"membership",
|
||||
"decisionApproaches",
|
||||
"conflictManagement",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* `finalReview.json.categories` ships a demo ordering + localized names
|
||||
* (Values / Communication / Membership / Decision-making / Conflict
|
||||
* management). We reuse that ordering for the state-derived rows so the
|
||||
* Rule layout stays stable across customize / use-without-changes /
|
||||
* plain-custom flows, and fall back to the demo chips when state resolves
|
||||
* to nothing selected.
|
||||
*/
|
||||
function readFallbackCategoryRows(
|
||||
categories: readonly { name: string; chips: readonly string[] }[],
|
||||
): {
|
||||
names: {
|
||||
values: string;
|
||||
communication: string;
|
||||
membership: string;
|
||||
decisions: string;
|
||||
conflict: string;
|
||||
};
|
||||
rows: FinalReviewCategoryRowDetailed[];
|
||||
} {
|
||||
function categoryNamesFromMessages(
|
||||
categories: readonly { name: string }[],
|
||||
): FinalReviewCategoryNames {
|
||||
const get = (i: number): string =>
|
||||
typeof categories[i]?.name === "string" ? categories[i].name : "";
|
||||
return {
|
||||
names: {
|
||||
values: get(0),
|
||||
communication: get(1),
|
||||
membership: get(2),
|
||||
decisions: get(3),
|
||||
conflict: get(4),
|
||||
},
|
||||
rows: categories.map((c) => ({
|
||||
name: c.name,
|
||||
groupKey: null,
|
||||
entries: [...c.chips].map((label) => ({
|
||||
label,
|
||||
groupKey: null,
|
||||
overrideKey: null,
|
||||
})),
|
||||
})),
|
||||
values: get(0),
|
||||
communication: get(1),
|
||||
membership: get(2),
|
||||
decisions: get(3),
|
||||
conflict: get(4),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,28 +108,32 @@ export function FinalReviewScreen({
|
||||
);
|
||||
|
||||
const { categories: finalReviewCategories } = useMemo(() => {
|
||||
const { names, rows: fallbackRows } = readFallbackCategoryRows(
|
||||
m.create.reviewAndComplete.finalReview.categories,
|
||||
);
|
||||
const derived = buildFinalReviewCategoryRowsDetailed(state, names);
|
||||
const copy = m.create.reviewAndComplete.finalReview;
|
||||
const names = categoryNamesFromMessages(copy.categories);
|
||||
const rowsToRender: readonly FinalReviewCategoryRowDetailed[] =
|
||||
derived.length > 0 ? derived : fallbackRows;
|
||||
const usingFallbackRows = derived.length === 0;
|
||||
buildFinalReviewCategoryRowsDetailed(state, names);
|
||||
|
||||
const lookup = new Map<
|
||||
string,
|
||||
{ target: FinalReviewChipEditTarget | null; readOnly: TemplateChipDetail }
|
||||
>();
|
||||
|
||||
const cats: Category[] = rowsToRender.map((row, rowIndex) => {
|
||||
const effectiveGroupKey: TemplateFacetGroupKey | null =
|
||||
row.groupKey ??
|
||||
(usingFallbackRows && rowIndex < FACET_FALLBACK_ORDER.length
|
||||
? FACET_FALLBACK_ORDER[rowIndex]
|
||||
: null);
|
||||
|
||||
const cats: Category[] = rowsToRender.map((row) => {
|
||||
const effectiveGroupKey: TemplateFacetGroupKey | null = row.groupKey;
|
||||
const reviewReturn =
|
||||
variant === "editPublished" ? ("edit-rule" as const) : ("final-review" as const);
|
||||
const isEmpty = row.entries.length === 0;
|
||||
const flag =
|
||||
isEmpty && effectiveGroupKey === "coreValues"
|
||||
? copy.flags.noValues
|
||||
: isEmpty && effectiveGroupKey != null
|
||||
? copy.flags.noCard
|
||||
: undefined;
|
||||
const addButtonText = isEmpty
|
||||
? effectiveGroupKey === "coreValues"
|
||||
? copy.addValue
|
||||
: copy.addCard
|
||||
: "";
|
||||
|
||||
const chipOptions = row.entries.map((entry, idx) => {
|
||||
const chipId = `${row.name}-${idx}`;
|
||||
@@ -190,12 +157,19 @@ export function FinalReviewScreen({
|
||||
id: chipId,
|
||||
label: entry.label,
|
||||
state: "unselected" as const,
|
||||
hasWarning: finalReviewChipHasWarning(
|
||||
state,
|
||||
entry.groupKey,
|
||||
entry.overrideKey,
|
||||
),
|
||||
};
|
||||
});
|
||||
return {
|
||||
name: row.name,
|
||||
chipOptions,
|
||||
addButton: effectiveGroupKey != null,
|
||||
addButtonText,
|
||||
flag,
|
||||
onChipClick: (_categoryName: string, chipId: string) => {
|
||||
const hit = lookup.get(chipId);
|
||||
if (!hit) return;
|
||||
@@ -219,7 +193,7 @@ export function FinalReviewScreen({
|
||||
});
|
||||
return { categories: cats };
|
||||
}, [
|
||||
m.create.reviewAndComplete.finalReview.categories,
|
||||
m.create.reviewAndComplete.finalReview,
|
||||
state,
|
||||
markCreateFlowInteraction,
|
||||
goToStep,
|
||||
@@ -249,8 +223,7 @@ export function FinalReviewScreen({
|
||||
const rawTitleForModal =
|
||||
typeof state.title === "string" ? state.title : "";
|
||||
|
||||
const descriptionEmptyHint =
|
||||
variant === "editPublished" ? t("communityContextEditModal.emptyHint") : undefined;
|
||||
const descriptionEmptyHint = t("communityContextEditModal.emptyHint");
|
||||
|
||||
const avatarUrl = useCommunityAvatarDisplayUrl(state.communityAvatarUrl);
|
||||
const storedFallback =
|
||||
@@ -305,34 +278,21 @@ export function FinalReviewScreen({
|
||||
<Rule
|
||||
title={ruleCardTitle}
|
||||
description={ruleCardDescription}
|
||||
onTitleClick={
|
||||
variant === "editPublished"
|
||||
? () => setTitleModalOpen(true)
|
||||
: undefined
|
||||
}
|
||||
titleEditAriaLabel={
|
||||
variant === "editPublished"
|
||||
? t("titleEditModal.ariaEditTitle")
|
||||
: undefined
|
||||
}
|
||||
onDescriptionClick={
|
||||
variant === "editPublished"
|
||||
? () => setCommunityContextModalOpen(true)
|
||||
: undefined
|
||||
}
|
||||
onTitleClick={() => setTitleModalOpen(true)}
|
||||
titleEditAriaLabel={t("titleEditModal.ariaEditTitle")}
|
||||
onDescriptionClick={() => setCommunityContextModalOpen(true)}
|
||||
descriptionEmptyHint={descriptionEmptyHint}
|
||||
descriptionEditAriaLabel={
|
||||
variant === "editPublished"
|
||||
? t("communityContextEditModal.ariaEditDescription")
|
||||
: undefined
|
||||
}
|
||||
descriptionEditAriaLabel={t("communityContextEditModal.ariaEditDescription")}
|
||||
size={mdUp ? "L" : "M"}
|
||||
expanded={true}
|
||||
descriptionAtBottom
|
||||
backgroundColor="bg-transparent"
|
||||
surfaceColor={surfaceColor}
|
||||
logoUrl={avatarUrl ?? getAssetPath(vectorMarkPath("mutual-aid"))}
|
||||
logoUrl={avatarUrl}
|
||||
logoAlt={ruleCardTitle}
|
||||
communityInitials={
|
||||
avatarUrl ? undefined : communityInitialsFromTitle(ruleCardTitle)
|
||||
}
|
||||
categories={finalReviewCategories}
|
||||
className={`${CREATE_FLOW_REVIEW_RULE_LAYOUT_CLASS}${surfaceColor ? "" : " invisible"}`}
|
||||
onClick={() => {}}
|
||||
@@ -352,28 +312,24 @@ export function FinalReviewScreen({
|
||||
onClose={() => setActiveReadOnlyDetail(null)}
|
||||
detail={activeReadOnlyDetail}
|
||||
/>
|
||||
{variant === "editPublished" ? (
|
||||
<>
|
||||
<FinalReviewTitleEditModal
|
||||
isOpen={titleModalOpen}
|
||||
onClose={() => setTitleModalOpen(false)}
|
||||
initialValue={rawTitleForModal}
|
||||
onSave={(value) => {
|
||||
markCreateFlowInteraction();
|
||||
updateState({ title: value });
|
||||
}}
|
||||
/>
|
||||
<FinalReviewCommunityContextEditModal
|
||||
isOpen={communityContextModalOpen}
|
||||
onClose={() => setCommunityContextModalOpen(false)}
|
||||
initialValue={rawCommunityContextForModal}
|
||||
onSave={(value) => {
|
||||
markCreateFlowInteraction();
|
||||
updateState({ communityContext: value, summary: value });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<FinalReviewTitleEditModal
|
||||
isOpen={titleModalOpen}
|
||||
onClose={() => setTitleModalOpen(false)}
|
||||
initialValue={rawTitleForModal}
|
||||
onSave={(value) => {
|
||||
markCreateFlowInteraction();
|
||||
updateState({ title: value });
|
||||
}}
|
||||
/>
|
||||
<FinalReviewCommunityContextEditModal
|
||||
isOpen={communityContextModalOpen}
|
||||
onClose={() => setCommunityContextModalOpen(false)}
|
||||
initialValue={rawCommunityContextForModal}
|
||||
onSave={(value) => {
|
||||
markCreateFlowInteraction();
|
||||
updateState({ communityContext: value, summary: value });
|
||||
}}
|
||||
/>
|
||||
</CreateFlowLockupCardStepShell>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import type footerMessages from "../../../../messages/en/create/footer.json";
|
||||
import type { CreateFlowStep } from "../types";
|
||||
import {
|
||||
buildFinalReviewCategoryRowsDetailed,
|
||||
type FinalReviewCategoryNames,
|
||||
} from "../../../../lib/create/buildFinalReviewCategories";
|
||||
import type { TemplateFacetGroupKey } from "../../../../lib/create/templateReviewMapping";
|
||||
import type { CreateFlowState, CreateFlowStep } from "../types";
|
||||
|
||||
type FooterMessages = typeof footerMessages;
|
||||
|
||||
@@ -9,9 +14,10 @@ type FooterMessages = typeof footerMessages;
|
||||
* fires `goToNextStep` for steps without a bespoke footer). Steps absent
|
||||
* from this map fall back to `footer.next`.
|
||||
*
|
||||
* `final-review` is handled separately by the caller because its label
|
||||
* also depends on the in-flight publish flag (`publishButtonPublishing`
|
||||
* vs `publishCommunityRule`).
|
||||
* `final-review` uses Continue (gated by {@link isFinalReviewContinueBlocked}).
|
||||
* `edit-rule` is handled separately by the caller because its label also
|
||||
* depends on the in-flight publish flag (`publishButtonPublishing` vs
|
||||
* `publishCommunityRule`).
|
||||
*/
|
||||
const DEFAULT_FOOTER_LABEL_BY_STEP: ReadonlyMap<
|
||||
CreateFlowStep,
|
||||
@@ -53,10 +59,42 @@ export function isCommunitySizeStepBlocked(
|
||||
return !hasNonEmptyId(selectedCommunitySizeIds);
|
||||
}
|
||||
|
||||
const FINAL_REVIEW_BLOCK_NAMES: FinalReviewCategoryNames = {
|
||||
values: "values",
|
||||
communication: "communication",
|
||||
membership: "membership",
|
||||
decisions: "decisions",
|
||||
conflict: "conflict",
|
||||
};
|
||||
|
||||
const FINAL_REVIEW_REQUIRED_GROUPS: readonly TemplateFacetGroupKey[] = [
|
||||
"communication",
|
||||
"membership",
|
||||
"decisionApproaches",
|
||||
"conflictManagement",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Continue on final review waits for a community name and at least one card
|
||||
* on each of the four questions. Skipped values do not block.
|
||||
*/
|
||||
export function isFinalReviewContinueBlocked(state: CreateFlowState): boolean {
|
||||
const title = typeof state.title === "string" ? state.title.trim() : "";
|
||||
if (title.length === 0) return true;
|
||||
const rows = buildFinalReviewCategoryRowsDetailed(
|
||||
state,
|
||||
FINAL_REVIEW_BLOCK_NAMES,
|
||||
);
|
||||
return FINAL_REVIEW_REQUIRED_GROUPS.some((groupKey) => {
|
||||
const row = rows.find((candidate) => candidate.groupKey === groupKey);
|
||||
return !row || row.entries.length === 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the localized label for the default "next-step" footer button.
|
||||
* Returns the per-step override when one is registered, otherwise
|
||||
* `footer.next`. Caller still owns the `final-review` special case.
|
||||
* `footer.next`. Caller still owns the `final-review` / `edit-rule` special case.
|
||||
*/
|
||||
export function getDefaultFooterLabel(
|
||||
step: CreateFlowStep | null | undefined,
|
||||
|
||||
@@ -21,8 +21,8 @@ const PROPORTION_BY_STEP_INDEX: readonly ProportionBarState[] = [
|
||||
"2-2", // membership-methods (Figma — Compact Card Stack `20858:13947`)
|
||||
"2-3", // decision-approaches (Figma Flow — Right Rail `20523:23509`)
|
||||
"3-0", // conflict-management (Figma Flow — Compact Card Stack `20879:15979`; start of Review segment)
|
||||
"3-2", // confirm-stakeholders (same fill as final-review)
|
||||
"3-2", // final-review
|
||||
"3-2", // confirm-stakeholders (same fill as final-review)
|
||||
"3-2", // completed
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -123,18 +123,18 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
||||
messageNamespace: "create.customRule.conflictManagement",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"final-review": {
|
||||
layoutKind: "review",
|
||||
figmaNodeId: "20907-213155",
|
||||
messageNamespace: "create.reviewAndComplete.finalReview",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"confirm-stakeholders": {
|
||||
layoutKind: "select",
|
||||
figmaNodeId: "21104-46594",
|
||||
messageNamespace: "create.reviewAndComplete.confirmStakeholders",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"final-review": {
|
||||
layoutKind: "review",
|
||||
figmaNodeId: "20907-212767",
|
||||
messageNamespace: "create.reviewAndComplete.finalReview",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"edit-rule": {
|
||||
layoutKind: "review",
|
||||
figmaNodeId: "20907-212767",
|
||||
|
||||
@@ -27,8 +27,8 @@ export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
|
||||
"membership-methods",
|
||||
"decision-approaches",
|
||||
"conflict-management",
|
||||
"confirm-stakeholders",
|
||||
"final-review",
|
||||
"confirm-stakeholders",
|
||||
"completed",
|
||||
] as const;
|
||||
|
||||
@@ -61,7 +61,7 @@ export type CreateFlowNavigationOptions = {
|
||||
* Community + custom-rule steps skipped when
|
||||
* {@link CreateFlowNavigationOptions.useWithoutChangesIdentityOnly} is set.
|
||||
* Remaining identity steps: `community-name` → `community-context` →
|
||||
* `community-upload`, then `confirm-stakeholders`.
|
||||
* `community-upload`, then `final-review`.
|
||||
*/
|
||||
const USE_WITHOUT_CHANGES_IDENTITY_SKIP = new Set<CreateFlowStep>([
|
||||
"informational",
|
||||
@@ -131,9 +131,9 @@ export function getPreviousStep(
|
||||
* Template **Use without changes** exceptions:
|
||||
* - Direct from a template (`useWithoutChangesIdentityOnly`): Back from
|
||||
* `community-name` returns to template review; Back from
|
||||
* `confirm-stakeholders` is `community-upload`.
|
||||
* `final-review` is `community-upload`.
|
||||
* - In-flow (`?fromFlow=1`, community identity already collected): Back
|
||||
* from `confirm-stakeholders` returns to template review instead of
|
||||
* from `final-review` returns to template review instead of
|
||||
* `conflict-management`.
|
||||
*/
|
||||
export type CreateFlowBackTarget =
|
||||
@@ -157,7 +157,7 @@ export function resolveCreateFlowBackTarget(
|
||||
return { kind: "templateReview", slug };
|
||||
}
|
||||
if (
|
||||
currentStep === "confirm-stakeholders" &&
|
||||
currentStep === "final-review" &&
|
||||
slug.length > 0 &&
|
||||
!options?.useWithoutChangesIdentityOnly
|
||||
) {
|
||||
|
||||
@@ -6,6 +6,10 @@ export interface Category {
|
||||
chipOptions: ChipOption[];
|
||||
/** When `false`, hide the row’s + affordance. Default: show when the Rule allows adds. */
|
||||
addButton?: boolean;
|
||||
/** Label on the add control. Empty string keeps the icon-only circle. */
|
||||
addButtonText?: string;
|
||||
/** Figma Has Flag: short line after the add chip in the same row. */
|
||||
flag?: string;
|
||||
onChipClick?: (categoryName: string, chipId: string) => void;
|
||||
onAddClick?: (categoryName: string) => void;
|
||||
onCustomChipConfirm?: (
|
||||
|
||||
@@ -472,7 +472,8 @@ export function RuleView({
|
||||
addButton={
|
||||
!hideCategoryAddButton && category.addButton !== false
|
||||
}
|
||||
addButtonText="" // Empty text for icon-only circular button
|
||||
addButtonText={category.addButtonText ?? ""}
|
||||
flag={category.flag}
|
||||
className="w-full"
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -21,6 +21,7 @@ const ChipContainer = memo<ChipProps>(
|
||||
onRemove,
|
||||
onCheck,
|
||||
onClose,
|
||||
hasWarning = false,
|
||||
ariaLabel,
|
||||
}) => {
|
||||
const t = useTranslation("controlsChrome");
|
||||
@@ -89,11 +90,19 @@ const ChipContainer = memo<ChipProps>(
|
||||
onRemove={onRemove}
|
||||
onCheck={handleCheck}
|
||||
onClose={handleClose}
|
||||
hasWarning={hasWarning}
|
||||
inputValue={isCustom ? inputValue : undefined}
|
||||
onInputChange={isCustom ? setInputValue : undefined}
|
||||
onInputKeyDown={isCustom ? handleKeyDown : undefined}
|
||||
inputRef={isCustom ? inputRef : undefined}
|
||||
ariaLabel={ariaLabel ?? (isCustom ? undefined : label)}
|
||||
ariaLabel={
|
||||
ariaLabel ??
|
||||
(isCustom
|
||||
? undefined
|
||||
: hasWarning
|
||||
? t("chipMissingWords").replace("{label}", label)
|
||||
: label)
|
||||
}
|
||||
confirmAriaLabel={t("chipConfirm")}
|
||||
typeToAddPlaceholder={t("chipTypeToAdd")}
|
||||
closeAriaLabel={t("chipClose")}
|
||||
|
||||
@@ -48,6 +48,11 @@ export interface ChipProps {
|
||||
* Optional callback when custom chip is closed/removed.
|
||||
*/
|
||||
onClose?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
/**
|
||||
* Figma Has Warning: show a warning icon in place of the selected check.
|
||||
* Used on final review when a card or value is missing its words.
|
||||
*/
|
||||
hasWarning?: boolean;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
@@ -62,6 +67,7 @@ export interface ChipViewProps {
|
||||
onRemove?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onCheck?: (value: string, event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onClose?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
hasWarning?: boolean;
|
||||
inputValue?: string;
|
||||
onInputChange?: (value: string) => void;
|
||||
onInputKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { memo, type CSSProperties } from "react";
|
||||
import Icon from "../../asset/icon/Icon";
|
||||
import type { ChipViewProps } from "./Chip.types";
|
||||
|
||||
function chipSurfaceStyle(
|
||||
@@ -87,6 +88,7 @@ function ChipView({
|
||||
onRemove,
|
||||
onCheck,
|
||||
onClose,
|
||||
hasWarning = false,
|
||||
inputValue,
|
||||
onInputChange,
|
||||
onInputKeyDown,
|
||||
@@ -280,7 +282,13 @@ function ChipView({
|
||||
onClick={handleClick}
|
||||
{...sharedA11y}
|
||||
>
|
||||
{isSelected ? (
|
||||
{hasWarning ? (
|
||||
<Icon
|
||||
name="warning"
|
||||
size={isSmall ? 12 : 16}
|
||||
className="shrink-0 text-current"
|
||||
/>
|
||||
) : isSelected ? (
|
||||
<svg
|
||||
aria-hidden
|
||||
viewBox="0 0 12 12"
|
||||
|
||||
@@ -20,6 +20,7 @@ const MultiSelectContainer = memo<MultiSelectProps>(
|
||||
onAddClick,
|
||||
addButton: addButtonProp = true,
|
||||
addButtonText,
|
||||
flag,
|
||||
formHeader = true,
|
||||
helperText,
|
||||
errorMessage,
|
||||
@@ -45,11 +46,13 @@ const MultiSelectContainer = memo<MultiSelectProps>(
|
||||
onAddClick={onAddClick}
|
||||
addButton={addButtonProp}
|
||||
addButtonText={addButtonText}
|
||||
flag={flag}
|
||||
addButtonAriaLabel={
|
||||
addButtonText || t("multiSelectAddFallback")
|
||||
}
|
||||
chipSelectAriaLabel={t("chipSelect")}
|
||||
chipDeselectAriaLabel={t("chipDeselect")}
|
||||
chipMissingWordsAriaLabel={t("chipMissingWords")}
|
||||
formHeader={formHeader}
|
||||
helperText={helperText}
|
||||
errorMessage={errorMessage}
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface ChipOption {
|
||||
id: string;
|
||||
label: string;
|
||||
state?: ChipStateValue;
|
||||
/** Figma Has Warning on Control / Chip. */
|
||||
hasWarning?: boolean;
|
||||
}
|
||||
|
||||
export type MultiSelectSizeValue = "s" | "m";
|
||||
@@ -49,6 +51,11 @@ export interface MultiSelectProps {
|
||||
* Text for the add button
|
||||
*/
|
||||
addButtonText?: string;
|
||||
/**
|
||||
* Figma Has Flag: short line after the add chip in the same row, with no
|
||||
* fill and no extra height.
|
||||
*/
|
||||
flag?: string;
|
||||
/**
|
||||
* Whether to show form header (label and help icon) above multi-select (Figma prop).
|
||||
* @default true
|
||||
@@ -93,7 +100,9 @@ export interface MultiSelectViewProps {
|
||||
onAddClick?: () => void;
|
||||
addButton: boolean;
|
||||
addButtonText?: string;
|
||||
flag?: string;
|
||||
addButtonAriaLabel: string;
|
||||
chipMissingWordsAriaLabel: string;
|
||||
chipSelectAriaLabel: string;
|
||||
chipDeselectAriaLabel: string;
|
||||
formHeader: boolean;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { memo } from "react";
|
||||
import Chip from "../Chip";
|
||||
import Icon from "../../asset/icon/Icon";
|
||||
import InputLabel from "../../type/InputLabel";
|
||||
import type { MultiSelectViewProps } from "./MultiSelect.types";
|
||||
|
||||
@@ -15,9 +16,11 @@ function MultiSelectView({
|
||||
onAddClick,
|
||||
addButton,
|
||||
addButtonText,
|
||||
flag,
|
||||
addButtonAriaLabel,
|
||||
chipSelectAriaLabel,
|
||||
chipDeselectAriaLabel,
|
||||
chipMissingWordsAriaLabel,
|
||||
formHeader = true,
|
||||
helperText: helperTextProp,
|
||||
errorMessage,
|
||||
@@ -94,13 +97,19 @@ function MultiSelectView({
|
||||
palette={palette}
|
||||
size={chipSize}
|
||||
disabled={chipDisabled}
|
||||
hasWarning={option.hasWarning}
|
||||
ariaLabel={
|
||||
isCustom || !option.label
|
||||
? undefined
|
||||
: (isSelected
|
||||
? chipDeselectAriaLabel
|
||||
: chipSelectAriaLabel
|
||||
).replace("{label}", option.label)
|
||||
: option.hasWarning
|
||||
? chipMissingWordsAriaLabel.replace(
|
||||
"{label}",
|
||||
option.label,
|
||||
)
|
||||
: (isSelected
|
||||
? chipDeselectAriaLabel
|
||||
: chipSelectAriaLabel
|
||||
).replace("{label}", option.label)
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isCustom && !chipDisabled && onChipClick) {
|
||||
@@ -180,6 +189,22 @@ function MultiSelectView({
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{flag ? (
|
||||
<span
|
||||
className={`inline-flex items-center gap-[var(--space-100)] text-x-small-paragraph ${
|
||||
isInverse
|
||||
? "text-[color:var(--color-content-invert-primary)]"
|
||||
: "text-[color:var(--color-content-default-primary)]"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
name="warning"
|
||||
size={12}
|
||||
className="shrink-0 text-current"
|
||||
/>
|
||||
{flag}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{errorMessage ? (
|
||||
<p
|
||||
|
||||
Reference in new issue
Block a user