Select and upload templates
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import UploadView from "./Upload.view";
|
||||||
|
import type { UploadProps } from "./Upload.types";
|
||||||
|
|
||||||
|
const UploadContainer = memo<UploadProps>(
|
||||||
|
({
|
||||||
|
active = true,
|
||||||
|
label,
|
||||||
|
showHelpIcon = true,
|
||||||
|
onClick,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<UploadView
|
||||||
|
active={active}
|
||||||
|
label={label}
|
||||||
|
showHelpIcon={showHelpIcon}
|
||||||
|
onClick={onClick}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
UploadContainer.displayName = "Upload";
|
||||||
|
|
||||||
|
export default UploadContainer;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export interface UploadProps {
|
||||||
|
/**
|
||||||
|
* Whether the upload component is in active state.
|
||||||
|
* When active, button has white background with black text.
|
||||||
|
* When inactive, button has dark background with gray text.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
active?: boolean;
|
||||||
|
/**
|
||||||
|
* Label text displayed above the upload component
|
||||||
|
*/
|
||||||
|
label?: string;
|
||||||
|
/**
|
||||||
|
* Whether to show help icon next to label
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
showHelpIcon?: boolean;
|
||||||
|
/**
|
||||||
|
* Callback when upload button is clicked
|
||||||
|
*/
|
||||||
|
onClick?: () => void;
|
||||||
|
/**
|
||||||
|
* Additional CSS classes
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadViewProps {
|
||||||
|
active: boolean;
|
||||||
|
label?: string;
|
||||||
|
showHelpIcon: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import InputLabel from "../../utility/InputLabel";
|
||||||
|
import type { UploadViewProps } from "./Upload.types";
|
||||||
|
|
||||||
|
function UploadView({
|
||||||
|
active = true,
|
||||||
|
label,
|
||||||
|
showHelpIcon = true,
|
||||||
|
onClick,
|
||||||
|
className = "",
|
||||||
|
}: UploadViewProps) {
|
||||||
|
const isActive = active;
|
||||||
|
|
||||||
|
// Button styles based on active state
|
||||||
|
const buttonBgClass = isActive
|
||||||
|
? "bg-[var(--color-surface-invert-primary,white)]"
|
||||||
|
: "bg-[var(--color-surface-default-secondary,#141414)]";
|
||||||
|
|
||||||
|
const buttonTextColor = isActive
|
||||||
|
? "text-[color:var(--color-content-invert-primary,black)]"
|
||||||
|
: "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
|
||||||
|
|
||||||
|
// Description text color based on active state
|
||||||
|
const descriptionTextColor = isActive
|
||||||
|
? "text-[color:var(--color-content-default-primary,white)]"
|
||||||
|
: "text-[color:var(--color-content-default-tertiary,#b4b4b4)]";
|
||||||
|
|
||||||
|
// Icon color based on active state
|
||||||
|
const iconColor = isActive
|
||||||
|
? "text-[color:var(--color-content-invert-primary,black)]"
|
||||||
|
: "text-[color:var(--color-content-invert-tertiary,#2d2d2d)]";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col gap-[var(--measures-spacing-300,12px)] items-start relative w-full ${className}`}>
|
||||||
|
{/* Label using InputLabel component */}
|
||||||
|
{label && (
|
||||||
|
<InputLabel
|
||||||
|
label={label}
|
||||||
|
helpIcon={showHelpIcon}
|
||||||
|
asterisk={false}
|
||||||
|
helperText={false}
|
||||||
|
size="S"
|
||||||
|
palette="Default"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Upload container */}
|
||||||
|
<div className="bg-[var(--color-surface-default-secondary,#141414)] flex gap-[24px] items-center justify-center px-[var(--measures-spacing-600,24px)] py-[var(--measures-spacing-1200,48px)] rounded-[var(--measures-radius-200,8px)] shrink-0 w-full">
|
||||||
|
{/* Upload button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={`${buttonBgClass} flex gap-[var(--measures-spacing-150,6px)] items-center justify-center overflow-clip p-[var(--measures-spacing-300,12px)] rounded-[var(--measures-radius-full,9999px)] shrink-0 hover:opacity-80 transition-opacity`}
|
||||||
|
aria-label="Upload"
|
||||||
|
>
|
||||||
|
{/* Upload icon */}
|
||||||
|
<div className={`relative shrink-0 size-[20px] ${iconColor}`}>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="size-full"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
points="17 8 12 3 7 8"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
y1="3"
|
||||||
|
x2="12"
|
||||||
|
y2="15"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/* Button text */}
|
||||||
|
<div className={`flex flex-col font-inter font-medium justify-center leading-[0] relative shrink-0 text-[length:var(--sizing-400,16px)] whitespace-nowrap ${buttonTextColor}`}>
|
||||||
|
<p className="leading-[20px]">Upload</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Description text */}
|
||||||
|
<div className={`flex flex-[1_0_0] flex-col font-inter font-normal h-[32px] justify-center leading-[0] min-h-px min-w-px relative text-[length:var(--sizing-350,14px)] ${descriptionTextColor}`}>
|
||||||
|
<p className="leading-[20px] whitespace-pre-wrap">
|
||||||
|
Add images, PDFs, and other files to the policy
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadView.displayName = "UploadView";
|
||||||
|
|
||||||
|
export default memo(UploadView);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./Upload.container";
|
||||||
|
export type { UploadProps } from "./Upload.types";
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useMediaQuery } from "../../hooks/useMediaQuery";
|
||||||
|
import HeaderLockup from "../../components/type/HeaderLockup";
|
||||||
|
import MultiSelect from "../../components/controls/MultiSelect";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select page for the create flow
|
||||||
|
*
|
||||||
|
* Displays selection options using HeaderLockup and MultiSelect components.
|
||||||
|
* Responsive layout: two-column at 640px+, single column below 640px.
|
||||||
|
* Responsive sizing: uses L/M for HeaderLockup and S for MultiSelect based on 640px breakpoint.
|
||||||
|
*/
|
||||||
|
export default function SelectPage() {
|
||||||
|
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
|
||||||
|
|
||||||
|
// Sample options for MultiSelect components
|
||||||
|
const [communitySizeOptions, setCommunitySizeOptions] = useState([
|
||||||
|
{ id: "1", label: "1 member", state: "Unselected" as const },
|
||||||
|
{ id: "2", label: "2-10 members", state: "Unselected" as const },
|
||||||
|
{ id: "3", label: "10-24 members", state: "Unselected" as const },
|
||||||
|
{ id: "4", label: "24-64 members", state: "Unselected" as const },
|
||||||
|
{ id: "5", label: "64-128 members", state: "Unselected" as const },
|
||||||
|
{ id: "6", label: "125-1000 members", state: "Unselected" as const },
|
||||||
|
{ id: "7", label: "1000+ members", state: "Unselected" as const },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [organizationTypeOptions, setOrganizationTypeOptions] = useState([
|
||||||
|
{ id: "1", label: "Non-profit", state: "Unselected" as const },
|
||||||
|
{ id: "2", label: "For-profit", state: "Unselected" as const },
|
||||||
|
{ id: "3", label: "Community", state: "Unselected" as const },
|
||||||
|
{ id: "4", label: "Educational", state: "Unselected" as const },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [governanceStyleOptions, setGovernanceStyleOptions] = useState([
|
||||||
|
{ id: "1", label: "Democratic", state: "Unselected" as const },
|
||||||
|
{ id: "2", label: "Consensus", state: "Unselected" as const },
|
||||||
|
{ id: "3", label: "Hierarchical", state: "Unselected" as const },
|
||||||
|
{ id: "4", label: "Flat", state: "Unselected" as const },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleCommunitySizeClick = (chipId: string) => {
|
||||||
|
setCommunitySizeOptions((prev) =>
|
||||||
|
prev.map((opt) =>
|
||||||
|
opt.id === chipId
|
||||||
|
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
|
||||||
|
: opt
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrganizationTypeClick = (chipId: string) => {
|
||||||
|
setOrganizationTypeOptions((prev) =>
|
||||||
|
prev.map((opt) =>
|
||||||
|
opt.id === chipId
|
||||||
|
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
|
||||||
|
: opt
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGovernanceStyleClick = (chipId: string) => {
|
||||||
|
setGovernanceStyleOptions((prev) =>
|
||||||
|
prev.map((opt) =>
|
||||||
|
opt.id === chipId
|
||||||
|
? { ...opt, state: opt.state === "Selected" ? "Unselected" : "Selected" }
|
||||||
|
: opt
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col items-center px-[var(--spacing-measures-spacing-500,20px)] md:px-[64px]">
|
||||||
|
{isMdOrLarger ? (
|
||||||
|
// Two-column layout for 640px+
|
||||||
|
<div className="flex gap-[var(--measures-spacing-1200,48px)] items-center justify-center w-full max-w-[1280px]">
|
||||||
|
{/* Left column: HeaderLockup */}
|
||||||
|
<div className="flex flex-[1_0_0] flex-col gap-[var(--measures-spacing-200,8px)] items-start justify-center max-w-[640px] min-h-px min-w-px py-[12px]">
|
||||||
|
<HeaderLockup
|
||||||
|
title="What is your community called?"
|
||||||
|
description="This will be the name of your community"
|
||||||
|
justification="left"
|
||||||
|
size="L"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column: Three MultiSelect components */}
|
||||||
|
<div className="flex flex-[1_0_0] flex-col gap-[var(--measures-spacing-800,32px)] items-start max-w-[640px] min-h-px min-w-px">
|
||||||
|
<MultiSelect
|
||||||
|
label="Label"
|
||||||
|
size="S"
|
||||||
|
options={communitySizeOptions}
|
||||||
|
onChipClick={handleCommunitySizeClick}
|
||||||
|
addButton={true}
|
||||||
|
addButtonText="Add organization type"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="Label"
|
||||||
|
size="S"
|
||||||
|
options={organizationTypeOptions}
|
||||||
|
onChipClick={handleOrganizationTypeClick}
|
||||||
|
addButton={true}
|
||||||
|
addButtonText="Add organization type"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="Label"
|
||||||
|
size="S"
|
||||||
|
options={governanceStyleOptions}
|
||||||
|
onChipClick={handleGovernanceStyleClick}
|
||||||
|
addButton={true}
|
||||||
|
addButtonText="Add organization type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Single column layout below 640px
|
||||||
|
<div className="flex flex-col gap-[var(--measures-spacing-400,16px)] items-start w-full max-w-[640px]">
|
||||||
|
{/* HeaderLockup */}
|
||||||
|
<HeaderLockup
|
||||||
|
title="What is your community called?"
|
||||||
|
description="This will be the name of your community"
|
||||||
|
justification="left"
|
||||||
|
size="M"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Three MultiSelect components */}
|
||||||
|
<MultiSelect
|
||||||
|
label="Label"
|
||||||
|
size="S"
|
||||||
|
options={communitySizeOptions}
|
||||||
|
onChipClick={handleCommunitySizeClick}
|
||||||
|
addButton={true}
|
||||||
|
addButtonText="Add organization type"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="Label"
|
||||||
|
size="S"
|
||||||
|
options={organizationTypeOptions}
|
||||||
|
onChipClick={handleOrganizationTypeClick}
|
||||||
|
addButton={true}
|
||||||
|
addButtonText="Add organization type"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
label="Label"
|
||||||
|
size="S"
|
||||||
|
options={governanceStyleOptions}
|
||||||
|
onChipClick={handleGovernanceStyleClick}
|
||||||
|
addButton={true}
|
||||||
|
addButtonText="Add organization type"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMediaQuery } from "../../hooks/useMediaQuery";
|
||||||
|
import HeaderLockup from "../../components/type/HeaderLockup";
|
||||||
|
import Upload from "../../components/controls/Upload";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload page for the create flow
|
||||||
|
*
|
||||||
|
* Displays upload functionality using HeaderLockup and Upload components.
|
||||||
|
* Responsive layout: centered at 640px+, left-aligned below 640px.
|
||||||
|
* Responsive sizing: uses L/M for HeaderLockup based on 640px breakpoint.
|
||||||
|
*/
|
||||||
|
export default function UploadPage() {
|
||||||
|
const isMdOrLarger = useMediaQuery("(min-width: 640px)");
|
||||||
|
|
||||||
|
const handleUploadClick = () => {
|
||||||
|
// Handle upload button click
|
||||||
|
console.log("Upload clicked");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col items-center px-[var(--spacing-measures-spacing-500,20px)] md:px-[64px]">
|
||||||
|
<div className="flex flex-col gap-[18px] items-center w-full max-w-[640px]">
|
||||||
|
{/* HeaderLockup: Center justification at 640px+, left below 640px */}
|
||||||
|
<HeaderLockup
|
||||||
|
title="How should conflicts be resolved?"
|
||||||
|
description="This will be the name of your community"
|
||||||
|
justification={isMdOrLarger ? "center" : "left"}
|
||||||
|
size={isMdOrLarger ? "L" : "M"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Upload component: no label in create flow, max width 474px */}
|
||||||
|
<div className="w-full max-w-[474px]">
|
||||||
|
<Upload
|
||||||
|
active={true}
|
||||||
|
showHelpIcon={true}
|
||||||
|
onClick={handleUploadClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,23 +42,18 @@ export function useMediaQuery(
|
|||||||
mediaQuery = query;
|
mediaQuery = query;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize state with current match if available (SSR safety)
|
// Always start with false so server and first client render match (avoids hydration mismatch).
|
||||||
const [matches, setMatches] = useState(() => {
|
// Real value is set in useEffect after mount.
|
||||||
if (typeof window === "undefined") {
|
const [matches, setMatches] = useState(false);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return window.matchMedia(mediaQuery).matches;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if window is available (SSR safety)
|
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const media = window.matchMedia(mediaQuery);
|
const media = window.matchMedia(mediaQuery);
|
||||||
|
setMatches(media.matches);
|
||||||
|
|
||||||
// Create listener for changes
|
|
||||||
const listener = (event: MediaQueryListEvent) => {
|
const listener = (event: MediaQueryListEvent) => {
|
||||||
setMatches(event.matches);
|
setMatches(event.matches);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Upload from "../../app/components/controls/Upload";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Controls/Upload",
|
||||||
|
component: Upload,
|
||||||
|
parameters: {
|
||||||
|
layout: "centered",
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
"An upload component with active/inactive states. Displays a label, upload button with icon, and description text.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
active: {
|
||||||
|
control: { type: "boolean" },
|
||||||
|
description: "Whether the upload component is in active state",
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: { type: "text" },
|
||||||
|
description: "Label text displayed above the upload component",
|
||||||
|
},
|
||||||
|
showHelpIcon: {
|
||||||
|
control: { type: "boolean" },
|
||||||
|
description: "Whether to show help icon next to label",
|
||||||
|
},
|
||||||
|
onClick: { action: "clicked" },
|
||||||
|
},
|
||||||
|
tags: ["autodocs"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args) => <Upload {...args} />;
|
||||||
|
|
||||||
|
// Default story
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
Default.args = {
|
||||||
|
label: "Upload",
|
||||||
|
active: true,
|
||||||
|
showHelpIcon: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Active state
|
||||||
|
export const Active = Template.bind({});
|
||||||
|
Active.args = {
|
||||||
|
label: "Upload",
|
||||||
|
active: true,
|
||||||
|
showHelpIcon: true,
|
||||||
|
};
|
||||||
|
Active.parameters = {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Upload component in active state with white button and black text.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inactive state
|
||||||
|
export const Inactive = Template.bind({});
|
||||||
|
Inactive.args = {
|
||||||
|
label: "Upload",
|
||||||
|
active: false,
|
||||||
|
showHelpIcon: true,
|
||||||
|
};
|
||||||
|
Inactive.parameters = {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Upload component in inactive state with dark button and gray text.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Without help icon
|
||||||
|
export const WithoutHelpIcon = Template.bind({});
|
||||||
|
WithoutHelpIcon.args = {
|
||||||
|
label: "Upload",
|
||||||
|
active: true,
|
||||||
|
showHelpIcon: false,
|
||||||
|
};
|
||||||
|
WithoutHelpIcon.parameters = {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Upload component without help icon.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Without label
|
||||||
|
export const WithoutLabel = Template.bind({});
|
||||||
|
WithoutLabel.args = {
|
||||||
|
active: true,
|
||||||
|
showHelpIcon: false,
|
||||||
|
};
|
||||||
|
WithoutLabel.parameters = {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Upload component without label.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom label
|
||||||
|
export const CustomLabel = Template.bind({});
|
||||||
|
CustomLabel.args = {
|
||||||
|
label: "Upload Files",
|
||||||
|
active: true,
|
||||||
|
showHelpIcon: true,
|
||||||
|
};
|
||||||
|
CustomLabel.parameters = {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story: "Upload component with custom label text.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// All states comparison
|
||||||
|
export const AllStates = () => (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Upload States</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Upload label="Active State" active={true} showHelpIcon={true} />
|
||||||
|
<Upload label="Inactive State" active={false} showHelpIcon={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import Upload from "../../app/components/controls/Upload";
|
||||||
|
import { componentTestSuite } from "../utils/componentTestSuite";
|
||||||
|
|
||||||
|
type UploadProps = React.ComponentProps<typeof Upload>;
|
||||||
|
|
||||||
|
componentTestSuite<UploadProps>({
|
||||||
|
component: Upload,
|
||||||
|
name: "Upload",
|
||||||
|
props: {
|
||||||
|
label: "Upload",
|
||||||
|
active: true,
|
||||||
|
} as UploadProps,
|
||||||
|
requiredProps: [],
|
||||||
|
optionalProps: {
|
||||||
|
label: "Upload",
|
||||||
|
active: true,
|
||||||
|
showHelpIcon: true,
|
||||||
|
},
|
||||||
|
primaryRole: "button",
|
||||||
|
testCases: {
|
||||||
|
renders: true,
|
||||||
|
accessibility: true,
|
||||||
|
keyboardNavigation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Upload (behavioral tests)", () => {
|
||||||
|
it("renders with active state by default", () => {
|
||||||
|
render(<Upload label="Upload" />);
|
||||||
|
const button = screen.getByRole("button", { name: /upload/i });
|
||||||
|
expect(button).toHaveClass("bg-[var(--color-surface-invert-primary,white)]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders with inactive state when active is false", () => {
|
||||||
|
render(<Upload label="Upload" active={false} />);
|
||||||
|
const button = screen.getByRole("button", { name: /upload/i });
|
||||||
|
expect(button).toHaveClass("bg-[var(--color-surface-default-secondary,#141414)]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays label when provided", () => {
|
||||||
|
render(<Upload label="Upload files" />);
|
||||||
|
expect(screen.getByText("Upload files")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not display label when not provided", () => {
|
||||||
|
const { container } = render(<Upload />);
|
||||||
|
const label = container.querySelector('[data-name="utility/Input label"]');
|
||||||
|
expect(label).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows help icon when showHelpIcon is true", () => {
|
||||||
|
render(<Upload label="Upload" showHelpIcon={true} />);
|
||||||
|
const helpIcon = screen.getByAltText("Help");
|
||||||
|
expect(helpIcon).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides help icon when showHelpIcon is false", () => {
|
||||||
|
render(<Upload label="Upload" showHelpIcon={false} />);
|
||||||
|
const helpIcon = screen.queryByAltText("Help");
|
||||||
|
expect(helpIcon).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onClick when upload button is clicked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const handleClick = vi.fn();
|
||||||
|
render(<Upload label="Upload" onClick={handleClick} />);
|
||||||
|
const button = screen.getByRole("button", { name: /upload/i });
|
||||||
|
await user.click(button);
|
||||||
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays description text", () => {
|
||||||
|
render(<Upload label="Upload" />);
|
||||||
|
expect(screen.getByText(/Add images, PDFs, and other files to the policy/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies active state styles correctly", () => {
|
||||||
|
render(<Upload label="Upload" active={true} />);
|
||||||
|
const descriptionText = screen.getByText(/Add images, PDFs, and other files to the policy/i);
|
||||||
|
const descriptionContainer = descriptionText.parentElement;
|
||||||
|
expect(descriptionContainer).toHaveClass("text-[color:var(--color-content-default-primary,white)]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies inactive state styles correctly", () => {
|
||||||
|
render(<Upload label="Upload" active={false} />);
|
||||||
|
const descriptionText = screen.getByText(/Add images, PDFs, and other files to the policy/i);
|
||||||
|
const descriptionContainer = descriptionText.parentElement;
|
||||||
|
expect(descriptionContainer).toHaveClass("text-[color:var(--color-content-default-tertiary,#b4b4b4)]");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user