Drop Cookies Settings from the footer, link the Home features that already have pages, and give leftover routes their own descriptions.
187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import { memo } from "react";
|
|
import Chip from "../Chip";
|
|
import InputLabel from "../../type/InputLabel";
|
|
import type { MultiSelectViewProps } from "./MultiSelect.types";
|
|
|
|
function MultiSelectView({
|
|
label,
|
|
showHelpIcon,
|
|
size,
|
|
palette,
|
|
options,
|
|
onChipClick,
|
|
onAddClick,
|
|
addButton,
|
|
addButtonText,
|
|
addButtonAriaLabel,
|
|
chipSelectAriaLabel,
|
|
chipDeselectAriaLabel,
|
|
formHeader = true,
|
|
onCustomChipConfirm,
|
|
onCustomChipClose,
|
|
maxSelections,
|
|
selectionCountText,
|
|
limitReachedAnnouncement,
|
|
className,
|
|
}: MultiSelectViewProps) {
|
|
const isSmall = size === "s";
|
|
const isInverse = palette === "inverse";
|
|
|
|
// Size-based spacing
|
|
const gapClass = isSmall
|
|
? "gap-[var(--space-200)]"
|
|
: "gap-[var(--space-300)]";
|
|
|
|
const chipSize = size;
|
|
const selectedCount = options.filter((o) => o.state === "selected").length;
|
|
const atLimit =
|
|
maxSelections != null && selectedCount >= maxSelections;
|
|
const countCaption = selectionCountText?.trim() ?? "";
|
|
const helperText = countCaption.length > 0 ? countCaption : false;
|
|
|
|
return (
|
|
<div
|
|
className={`flex flex-col ${isSmall ? "gap-[var(--space-200)]" : "gap-[var(--space-300)]"} items-start relative w-full ${className}`}
|
|
>
|
|
{/* Label using InputLabel component */}
|
|
{formHeader && label && (
|
|
<InputLabel
|
|
label={label}
|
|
helpIcon={showHelpIcon}
|
|
asterisk={false}
|
|
helperText={helperText}
|
|
size={size}
|
|
palette={palette}
|
|
/>
|
|
)}
|
|
|
|
{!formHeader || !label ? (
|
|
countCaption.length > 0 ? (
|
|
<p className="w-full text-x-small-paragraph text-[color:var(--color-content-default-tertiary)]">
|
|
{countCaption}
|
|
</p>
|
|
) : null
|
|
) : null}
|
|
|
|
{limitReachedAnnouncement ? (
|
|
<p className="sr-only" aria-live="polite">
|
|
{atLimit ? limitReachedAnnouncement : ""}
|
|
</p>
|
|
) : null}
|
|
|
|
{/* Chips container */}
|
|
<div
|
|
className={`flex flex-wrap ${gapClass} items-center relative shrink-0 w-full`}
|
|
>
|
|
{options.map((option) => {
|
|
const isCustom = option.state === "custom";
|
|
const isSelected = option.state === "selected";
|
|
const chipDisabled = !isCustom && !isSelected && atLimit;
|
|
const chipState = chipDisabled
|
|
? "disabled"
|
|
: option.state || "unselected";
|
|
return (
|
|
<Chip
|
|
key={option.id}
|
|
label={isCustom ? "" : option.label}
|
|
state={chipState}
|
|
palette={palette}
|
|
size={chipSize}
|
|
disabled={chipDisabled}
|
|
ariaLabel={
|
|
isCustom || !option.label
|
|
? undefined
|
|
: (isSelected
|
|
? chipDeselectAriaLabel
|
|
: chipSelectAriaLabel
|
|
).replace("{label}", option.label)
|
|
}
|
|
onClick={() => {
|
|
if (!isCustom && !chipDisabled && onChipClick) {
|
|
onChipClick(option.id);
|
|
}
|
|
}}
|
|
onCheck={(value, e) => {
|
|
e.stopPropagation();
|
|
if (onCustomChipConfirm) {
|
|
onCustomChipConfirm(option.id, value);
|
|
}
|
|
}}
|
|
onClose={(e) => {
|
|
e.stopPropagation();
|
|
if (onCustomChipClose) {
|
|
onCustomChipClose(option.id);
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{/* Add button — icon-only: bordered circle + brand icon (chips stay yellow). With label: Figma 19688:38288 — brand + icon, primary label text, no fill/border. */}
|
|
{addButton && (
|
|
<button
|
|
type="button"
|
|
aria-label={addButtonAriaLabel}
|
|
disabled={atLimit}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
if (atLimit) return;
|
|
onAddClick?.();
|
|
}}
|
|
className={
|
|
!addButtonText
|
|
? // Circular button with border (Rule style)
|
|
`${atLimit ? "cursor-not-allowed opacity-60" : "cursor-pointer hover:opacity-80"} bg-[var(--color-surface-default-transparent)] border-[1.25px] ${isInverse ? "border-[var(--color-border-default-primary)]" : "border-[var(--color-border-default-control)]"} border-solid flex items-center justify-center ${isSmall ? "size-[30px]" : "size-[40px]"} rounded-[var(--radius-full)] shrink-0 transition-opacity`
|
|
: // Text add control (default palette: white label + brand “+”; inverse: inverse primary for both)
|
|
`${atLimit ? "cursor-not-allowed opacity-60" : "cursor-pointer hover:opacity-80"} flex items-center justify-center overflow-hidden rounded-[var(--radius-full)] shrink-0 transition-opacity ${
|
|
isSmall
|
|
? "gap-[var(--space-100)] px-[var(--space-300)] py-[var(--space-200)]"
|
|
: "gap-[var(--space-150)] px-[var(--space-400)] py-[var(--space-300)]"
|
|
}`
|
|
}
|
|
>
|
|
{/* Plus icon — brand accent; selection chips keep full yellow fill separately */}
|
|
<svg
|
|
width={isSmall ? "14" : "20"}
|
|
height={isSmall ? "14" : "20"}
|
|
viewBox="0 0 14 14"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className={`shrink-0 ${
|
|
!addButtonText && isInverse
|
|
? "text-[var(--color-content-invert-primary)]"
|
|
: "text-[var(--color-content-default-brand-primary)]"
|
|
}`}
|
|
>
|
|
<path
|
|
d="M7 3V11M3 7H11"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
{addButtonText && (
|
|
<span
|
|
className={`font-medium ${isSmall ? "text-[length:var(--sizing-300)] leading-[14px]" : "text-medium-label"} ${
|
|
isInverse
|
|
? "text-[color:var(--color-content-invert-primary)]"
|
|
: "text-[color:var(--color-content-default-primary)]"
|
|
}`}
|
|
>
|
|
{addButtonText}
|
|
</span>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
MultiSelectView.displayName = "MultiSelectView";
|
|
|
|
export default memo(MultiSelectView);
|