Merge pull request 'Tooltip and Alert Components' (#32) from adilallo/feature/TooltipAlertComponents into main
Reviewed-on: #32
This commit was merged in pull request #32.
This commit is contained in:
@@ -0,0 +1,184 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Tooltip from "../components/Tooltip";
|
||||||
|
import Alert from "../components/Alert";
|
||||||
|
import Button from "../components/Button";
|
||||||
|
|
||||||
|
export default function ComponentsPreview() {
|
||||||
|
const [alertVisible, setAlertVisible] = useState({
|
||||||
|
default: true,
|
||||||
|
positive: true,
|
||||||
|
warning: true,
|
||||||
|
danger: true,
|
||||||
|
banner: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-[var(--spacing-scale-032)]">
|
||||||
|
<div className="max-w-[1200px] mx-auto space-y-[var(--spacing-scale-064)]">
|
||||||
|
<header className="space-y-[var(--spacing-scale-008)]">
|
||||||
|
<h1 className="font-bricolage-grotesque text-[48px] leading-[56px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
|
Component Preview
|
||||||
|
</h1>
|
||||||
|
<p className="font-inter text-[18px] leading-[24px] text-[var(--color-content-default-secondary)]">
|
||||||
|
Temporary page for viewing and testing new components
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Tooltip Section */}
|
||||||
|
<section className="space-y-[var(--spacing-scale-024)]">
|
||||||
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
|
Tooltip Component
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||||
|
<div className="flex flex-wrap gap-[var(--spacing-scale-024)] items-center">
|
||||||
|
<Tooltip text="Tooltip positioned at top" position="top">
|
||||||
|
<Button variant="default" size="medium">
|
||||||
|
Hover me (Top)
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip text="Tooltip positioned at bottom" position="bottom">
|
||||||
|
<Button variant="primary" size="medium">
|
||||||
|
Hover me (Bottom)
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip text="Disabled tooltip" disabled>
|
||||||
|
<Button variant="secondary" size="medium">
|
||||||
|
Disabled Tooltip
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip text="Tooltip with icon button" position="top">
|
||||||
|
<button className="p-[var(--spacing-scale-012)] rounded-full hover:bg-[var(--color-surface-default-tertiary)] transition-colors">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 9V11M10 15H10.01M19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Alert Section */}
|
||||||
|
<section className="space-y-[var(--spacing-scale-024)]">
|
||||||
|
<h2 className="font-bricolage-grotesque text-[32px] leading-[40px] font-bold text-[var(--color-content-default-primary)]">
|
||||||
|
Alert Component
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-[var(--spacing-scale-024)]">
|
||||||
|
{/* Toast Alerts */}
|
||||||
|
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-016)]">
|
||||||
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||||
|
Toast Alerts
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{alertVisible.default && (
|
||||||
|
<Alert
|
||||||
|
title="Short alert banner message goes here"
|
||||||
|
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
||||||
|
status="default"
|
||||||
|
type="toast"
|
||||||
|
onClose={() =>
|
||||||
|
setAlertVisible({ ...alertVisible, default: false })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{alertVisible.positive && (
|
||||||
|
<Alert
|
||||||
|
title="Short alert banner message goes here"
|
||||||
|
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
||||||
|
status="positive"
|
||||||
|
type="toast"
|
||||||
|
onClose={() =>
|
||||||
|
setAlertVisible({ ...alertVisible, positive: false })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{alertVisible.warning && (
|
||||||
|
<Alert
|
||||||
|
title="Short alert banner message goes here"
|
||||||
|
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
||||||
|
status="warning"
|
||||||
|
type="toast"
|
||||||
|
onClose={() =>
|
||||||
|
setAlertVisible({ ...alertVisible, warning: false })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{alertVisible.danger && (
|
||||||
|
<Alert
|
||||||
|
title="Short alert banner message goes here"
|
||||||
|
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
||||||
|
status="danger"
|
||||||
|
type="toast"
|
||||||
|
onClose={() =>
|
||||||
|
setAlertVisible({ ...alertVisible, danger: false })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Banner Alerts */}
|
||||||
|
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-016)]">
|
||||||
|
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||||
|
Banner Alerts
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{alertVisible.banner && (
|
||||||
|
<Alert
|
||||||
|
title="Short alert banner message goes here"
|
||||||
|
description="Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse."
|
||||||
|
status="default"
|
||||||
|
type="banner"
|
||||||
|
onClose={() =>
|
||||||
|
setAlertVisible({ ...alertVisible, banner: false })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
title="Positive banner alert"
|
||||||
|
description="This is a positive banner message"
|
||||||
|
status="positive"
|
||||||
|
type="banner"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
title="Warning banner alert"
|
||||||
|
description="This is a warning banner message"
|
||||||
|
status="warning"
|
||||||
|
type="banner"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
title="Danger banner alert"
|
||||||
|
description="This is a danger banner message"
|
||||||
|
status="danger"
|
||||||
|
type="banner"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
import { AlertView } from "./Alert.view";
|
||||||
|
import type { AlertProps } from "./Alert.types";
|
||||||
|
|
||||||
|
const AlertContainer = memo<AlertProps>(
|
||||||
|
({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
status = "default",
|
||||||
|
type = "toast",
|
||||||
|
onClose,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
|
// Determine background and border colors based on status and type
|
||||||
|
const getStatusStyles = () => {
|
||||||
|
switch (status) {
|
||||||
|
case "positive":
|
||||||
|
return {
|
||||||
|
background: "bg-[var(--color-kiwi-kiwi0)]",
|
||||||
|
borderColor:
|
||||||
|
type === "toast"
|
||||||
|
? "var(--color-border-invert-positive-primary)"
|
||||||
|
: undefined,
|
||||||
|
titleColor: "text-[var(--color-content-invert-primary)]",
|
||||||
|
descriptionColor: "text-[var(--color-content-invert-secondary)]",
|
||||||
|
iconColor: "var(--color-kiwi-kiwi500)",
|
||||||
|
closeButtonColor: "text-[var(--color-content-invert-primary)]",
|
||||||
|
closeButtonIconColor: "var(--color-content-invert-primary)",
|
||||||
|
};
|
||||||
|
case "warning":
|
||||||
|
return {
|
||||||
|
background: "bg-[var(--color-yellow-yellow0)]",
|
||||||
|
borderColor:
|
||||||
|
type === "toast"
|
||||||
|
? "var(--color-border-invert-warning-primary)"
|
||||||
|
: undefined,
|
||||||
|
titleColor: "text-[var(--color-content-invert-primary)]",
|
||||||
|
descriptionColor: "text-[var(--color-content-invert-secondary)]",
|
||||||
|
iconColor: "var(--color-yellow-yellow500)",
|
||||||
|
closeButtonColor: "text-[var(--color-content-invert-primary)]",
|
||||||
|
closeButtonIconColor: "var(--color-content-invert-primary)",
|
||||||
|
};
|
||||||
|
case "danger":
|
||||||
|
return {
|
||||||
|
background: "bg-[var(--color-red-red0)]",
|
||||||
|
borderColor:
|
||||||
|
type === "toast"
|
||||||
|
? "var(--color-border-invert-negative-primary)"
|
||||||
|
: undefined,
|
||||||
|
titleColor: "text-[var(--color-content-invert-negative-primary)]",
|
||||||
|
descriptionColor:
|
||||||
|
"text-[var(--color-content-invert-negative-primary)]",
|
||||||
|
iconColor: "var(--color-red-red500)",
|
||||||
|
closeButtonColor:
|
||||||
|
"text-[var(--color-content-invert-negative-primary)]",
|
||||||
|
closeButtonIconColor: "var(--color-content-invert-primary)",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
background: "bg-[var(--color-surface-default-tertiary)]",
|
||||||
|
borderColor:
|
||||||
|
type === "toast"
|
||||||
|
? "var(--color-border-default-primary)"
|
||||||
|
: undefined,
|
||||||
|
titleColor: "text-[var(--color-content-default-primary)]",
|
||||||
|
descriptionColor: "text-[var(--color-content-default-primary)]",
|
||||||
|
iconColor: "var(--color-content-default-brand-primary)",
|
||||||
|
closeButtonColor: "text-[var(--color-content-default-primary)]",
|
||||||
|
closeButtonIconColor: "var(--color-content-default-brand-primary)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusStyles = getStatusStyles();
|
||||||
|
|
||||||
|
const containerClasses = `flex gap-[var(--space-300)] items-center ${
|
||||||
|
type === "toast"
|
||||||
|
? `pb-[var(--space-500)] pt-[var(--space-400)] px-[var(--space-1200)] rounded-tl-[var(--radius-200,8px)] rounded-tr-[var(--radius-200,8px)]`
|
||||||
|
: `px-[var(--spacing-scale-024)] py-[var(--spacing-scale-016)] rounded-[var(--radius-200,8px)]`
|
||||||
|
} ${statusStyles.background} border-solid`;
|
||||||
|
|
||||||
|
const containerStyle =
|
||||||
|
type === "toast" && statusStyles.borderColor
|
||||||
|
? {
|
||||||
|
borderBottomWidth: "var(--border-large)",
|
||||||
|
borderBottomColor: statusStyles.borderColor,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const titleClasses =
|
||||||
|
type === "banner"
|
||||||
|
? `font-inter text-[16px] leading-[20px] font-medium tracking-[0%] ${statusStyles.titleColor} relative shrink-0 w-full`
|
||||||
|
: `font-inter text-[18px] leading-[24px] font-medium tracking-[0%] ${statusStyles.titleColor} relative shrink-0 w-full`;
|
||||||
|
|
||||||
|
const descriptionClasses =
|
||||||
|
type === "banner"
|
||||||
|
? `font-inter text-[16px] leading-[24px] font-normal tracking-[0%] ${statusStyles.descriptionColor} relative shrink-0 w-full mt-[var(--spacing-scale-004)]`
|
||||||
|
: `font-inter text-[18px] leading-[23.4px] font-normal tracking-[0%] ${statusStyles.descriptionColor} relative shrink-0 w-full mt-[var(--spacing-scale-004)]`;
|
||||||
|
|
||||||
|
const closeButtonClasses = `flex gap-[var(--spacing-scale-006)] items-center justify-center overflow-clip p-[var(--spacing-scale-012)] rounded-[var(--radius-full)] shrink-0 hover:bg-[var(--color-surface-default-secondary)] transition-colors ${statusStyles.closeButtonColor}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertView
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
status={status}
|
||||||
|
type={type}
|
||||||
|
className={className}
|
||||||
|
containerClasses={containerClasses}
|
||||||
|
containerStyle={containerStyle}
|
||||||
|
titleClasses={titleClasses}
|
||||||
|
descriptionClasses={descriptionClasses}
|
||||||
|
iconColor={statusStyles.iconColor}
|
||||||
|
closeButtonClasses={closeButtonClasses}
|
||||||
|
closeButtonIconColor={statusStyles.closeButtonIconColor}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
AlertContainer.displayName = "Alert";
|
||||||
|
|
||||||
|
export default AlertContainer;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export interface AlertProps {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
status?: "default" | "positive" | "warning" | "danger";
|
||||||
|
type?: "toast" | "banner";
|
||||||
|
onClose?: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlertViewProps {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
status: "default" | "positive" | "warning" | "danger";
|
||||||
|
type: "toast" | "banner";
|
||||||
|
className: string;
|
||||||
|
containerClasses: string;
|
||||||
|
containerStyle?: React.CSSProperties;
|
||||||
|
titleClasses: string;
|
||||||
|
descriptionClasses: string;
|
||||||
|
iconColor: string;
|
||||||
|
closeButtonClasses: string;
|
||||||
|
closeButtonIconColor: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type { AlertViewProps } from "./Alert.types";
|
||||||
|
|
||||||
|
export function AlertView({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
status: _status,
|
||||||
|
type: _type,
|
||||||
|
className,
|
||||||
|
containerClasses,
|
||||||
|
containerStyle,
|
||||||
|
titleClasses,
|
||||||
|
descriptionClasses,
|
||||||
|
iconColor,
|
||||||
|
closeButtonClasses,
|
||||||
|
closeButtonIconColor,
|
||||||
|
onClose,
|
||||||
|
}: AlertViewProps) {
|
||||||
|
const getIcon = () => {
|
||||||
|
// Use the Icon_Alert.svg with dynamic fill color
|
||||||
|
// The SVG has a fill that we'll override with the iconColor
|
||||||
|
// Icon is 19x19px with 2.5px spacing in a 24x24px bounding box
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="19"
|
||||||
|
height="19"
|
||||||
|
viewBox="0 0 19 19"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9.49998 14.2307C9.72883 14.2307 9.92065 14.1533 10.0755 13.9985C10.2303 13.8437 10.3077 13.6519 10.3077 13.4231C10.3077 13.1942 10.2303 13.0024 10.0755 12.8476C9.92065 12.6928 9.72883 12.6154 9.49998 12.6154C9.27112 12.6154 9.0793 12.6928 8.9245 12.8476C8.7697 13.0024 8.6923 13.1942 8.6923 13.4231C8.6923 13.6519 8.7697 13.8437 8.9245 13.9985C9.0793 14.1533 9.27112 14.2307 9.49998 14.2307ZM8.75 10.5769H10.25V4.5769H8.75V10.5769ZM9.50165 19C8.18772 19 6.95268 18.7506 5.79655 18.252C4.6404 17.7533 3.63472 17.0765 2.7795 16.2217C1.92427 15.3669 1.24721 14.3616 0.748325 13.206C0.249442 12.0504 0 10.8156 0 9.50165C0 8.18772 0.249334 6.95268 0.748 5.79655C1.24667 4.6404 1.92342 3.63472 2.77825 2.7795C3.6331 1.92427 4.63834 1.24721 5.79398 0.748326C6.94959 0.249443 8.18437 0 9.4983 0C10.8122 0 12.0473 0.249334 13.2034 0.748001C14.3596 1.24667 15.3652 1.92342 16.2205 2.77825C17.0757 3.6331 17.7527 4.63834 18.2516 5.79398C18.7505 6.94959 19 8.18437 19 9.4983C19 10.8122 18.7506 12.0473 18.252 13.2034C17.7533 14.3596 17.0765 15.3652 16.2217 16.2205C15.3669 17.0757 14.3616 17.7527 13.206 18.2516C12.0504 18.7505 10.8156 19 9.50165 19ZM9.49998 17.5C11.7333 17.5 13.625 16.725 15.175 15.175C16.725 13.625 17.5 11.7333 17.5 9.49998C17.5 7.26664 16.725 5.37498 15.175 3.82498C13.625 2.27498 11.7333 1.49998 9.49998 1.49998C7.26664 1.49998 5.37498 2.27498 3.82498 3.82498C2.27498 5.37498 1.49998 7.26664 1.49998 9.49998C1.49998 11.7333 2.27498 13.625 3.82498 15.175C5.37498 16.725 7.26664 17.5 9.49998 17.5Z"
|
||||||
|
fill={iconColor}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${containerClasses} ${className}`}
|
||||||
|
style={containerStyle}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div className="shrink-0 w-[24px] h-[24px] flex items-center justify-center">
|
||||||
|
{getIcon()}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col items-start justify-center min-h-0 min-w-0">
|
||||||
|
<p className={titleClasses}>{title}</p>
|
||||||
|
{description && <p className={descriptionClasses}>{description}</p>}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className={closeButtonClasses}
|
||||||
|
aria-label="Close alert"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<mask
|
||||||
|
id="mask0_21296_8285"
|
||||||
|
style={{ maskType: "alpha" }}
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
>
|
||||||
|
<rect width="20" height="20" fill="#D9D9D9" />
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_21296_8285)">
|
||||||
|
<path
|
||||||
|
d="M5.33327 15.5448L4.45508 14.6666L9.12174 9.99993L4.45508 5.33327L5.33327 4.45508L9.99993 9.12174L14.6666 4.45508L15.5448 5.33327L10.8781 9.99993L15.5448 14.6666L14.6666 15.5448L9.99993 10.8781L5.33327 15.5448Z"
|
||||||
|
fill={closeButtonIconColor}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./Alert.container";
|
||||||
|
export type { AlertProps } from "./Alert.types";
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { memo, useState } from "react";
|
||||||
|
import { TooltipView } from "./Tooltip.view";
|
||||||
|
import type { TooltipProps } from "./Tooltip.types";
|
||||||
|
|
||||||
|
const TooltipContainer = memo<TooltipProps>(
|
||||||
|
({ children, text, position = "top", className = "", disabled = false }) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipClasses = `absolute z-50 bg-[var(--color-surface-default-primary)] px-[var(--space-300)] py-[var(--space-200)] rounded-[var(--radius-300,12px)] shadow-[0px_0px_48px_0px_rgba(0,0,0,0.1)] flex items-center whitespace-nowrap ${
|
||||||
|
position === "top" ? "bottom-full mb-[7px]" : "top-full mt-[7px]"
|
||||||
|
} left-1/2 -translate-x-1/2 ${isVisible ? "opacity-100 visible" : "opacity-0 invisible pointer-events-none"} transition-all duration-200`;
|
||||||
|
|
||||||
|
// Pointer positioning: 10px tall, 7px sticks out, 3px inside tooltip
|
||||||
|
// For bottom tooltip: pointer at top, pointing up, 7px above tooltip
|
||||||
|
// For top tooltip: pointer at bottom, pointing down, 7px below tooltip
|
||||||
|
const pointerClasses = `absolute ${
|
||||||
|
position === "top" ? "bottom-[-7px]" : "top-[-7px]"
|
||||||
|
} left-1/2 -translate-x-1/2`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative inline-block ${className}`}
|
||||||
|
onMouseEnter={() => setIsVisible(true)}
|
||||||
|
onMouseLeave={() => setIsVisible(false)}
|
||||||
|
onFocus={() => setIsVisible(true)}
|
||||||
|
onBlur={() => setIsVisible(false)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipView
|
||||||
|
text={text}
|
||||||
|
position={position}
|
||||||
|
className=""
|
||||||
|
tooltipClasses={tooltipClasses}
|
||||||
|
pointerClasses={pointerClasses}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
TooltipContainer.displayName = "Tooltip";
|
||||||
|
|
||||||
|
export default TooltipContainer;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export interface TooltipProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
text: string;
|
||||||
|
position?: "top" | "bottom";
|
||||||
|
className?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TooltipViewProps {
|
||||||
|
text: string;
|
||||||
|
position: "top" | "bottom";
|
||||||
|
className: string;
|
||||||
|
tooltipClasses: string;
|
||||||
|
pointerClasses: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { TooltipViewProps } from "./Tooltip.types";
|
||||||
|
|
||||||
|
export function TooltipView({
|
||||||
|
text,
|
||||||
|
position,
|
||||||
|
className: _className,
|
||||||
|
tooltipClasses,
|
||||||
|
pointerClasses,
|
||||||
|
}: TooltipViewProps) {
|
||||||
|
// Pointer is 10px tall with 7px sticking out
|
||||||
|
// Icon_Pointer.svg is 14x8, scale to 10px height = 17.5px width
|
||||||
|
const pointerWidth = 17.5;
|
||||||
|
const pointerHeight = 10;
|
||||||
|
const pointerRotation = position === "top" ? "rotate-180" : "rotate-0";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={tooltipClasses}
|
||||||
|
role="tooltip"
|
||||||
|
aria-live="polite"
|
||||||
|
id={`tooltip-${text.replace(/\s+/g, "-").toLowerCase()}`}
|
||||||
|
>
|
||||||
|
<p className="font-inter text-[var(--sizing-350,14px)] leading-[16px] font-medium tracking-[0%] text-[var(--color-content-inverse-primary)] relative shrink-0">
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className={pointerClasses}
|
||||||
|
style={{ width: `${pointerWidth}px`, height: `${pointerHeight}px` }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`${pointerRotation} w-full h-full`}
|
||||||
|
viewBox="0 0 14 8"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.92822 0L13.8564 7.5H1.95503e-05L6.92822 0Z"
|
||||||
|
fill="var(--color-surface-default-primary)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./Tooltip.container";
|
||||||
|
export type { TooltipProps } from "./Tooltip.types";
|
||||||
@@ -55,4 +55,11 @@ export const ASSETS = {
|
|||||||
// Content page decorative shapes
|
// Content page decorative shapes
|
||||||
CONTENT_SHAPE_1: "assets/Content_Shape_1.svg",
|
CONTENT_SHAPE_1: "assets/Content_Shape_1.svg",
|
||||||
CONTENT_SHAPE_2: "assets/Content_Shape_2.svg",
|
CONTENT_SHAPE_2: "assets/Content_Shape_2.svg",
|
||||||
|
|
||||||
|
// Alert icons
|
||||||
|
ICON_ALERT: "assets/Icon_Alert.svg",
|
||||||
|
ICON_CLOSE: "assets/Icon_Close.svg",
|
||||||
|
|
||||||
|
// Tooltip icons
|
||||||
|
ICON_POINTER: "assets/Icon_Pointer.svg",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.49998 14.2307C9.72883 14.2307 9.92065 14.1533 10.0755 13.9985C10.2303 13.8437 10.3077 13.6519 10.3077 13.4231C10.3077 13.1942 10.2303 13.0024 10.0755 12.8476C9.92065 12.6928 9.72883 12.6154 9.49998 12.6154C9.27112 12.6154 9.0793 12.6928 8.9245 12.8476C8.7697 13.0024 8.6923 13.1942 8.6923 13.4231C8.6923 13.6519 8.7697 13.8437 8.9245 13.9985C9.0793 14.1533 9.27112 14.2307 9.49998 14.2307ZM8.75 10.5769H10.25V4.5769H8.75V10.5769ZM9.50165 19C8.18772 19 6.95268 18.7506 5.79655 18.252C4.6404 17.7533 3.63472 17.0765 2.7795 16.2217C1.92427 15.3669 1.24721 14.3616 0.748325 13.206C0.249442 12.0504 0 10.8156 0 9.50165C0 8.18772 0.249334 6.95268 0.748 5.79655C1.24667 4.6404 1.92342 3.63472 2.77825 2.7795C3.6331 1.92427 4.63834 1.24721 5.79398 0.748326C6.94959 0.249443 8.18437 0 9.4983 0C10.8122 0 12.0473 0.249334 13.2034 0.748001C14.3596 1.24667 15.3652 1.92342 16.2205 2.77825C17.0757 3.6331 17.7527 4.63834 18.2516 5.79398C18.7505 6.94959 19 8.18437 19 9.4983C19 10.8122 18.7506 12.0473 18.252 13.2034C17.7533 14.3596 17.0765 15.3652 16.2217 16.2205C15.3669 17.0757 14.3616 17.7527 13.206 18.2516C12.0504 18.7505 10.8156 19 9.50165 19ZM9.49998 17.5C11.7333 17.5 13.625 16.725 15.175 15.175C16.725 13.625 17.5 11.7333 17.5 9.49998C17.5 7.26664 16.725 5.37498 15.175 3.82498C13.625 2.27498 11.7333 1.49998 9.49998 1.49998C7.26664 1.49998 5.37498 2.27498 3.82498 3.82498C2.27498 5.37498 1.49998 7.26664 1.49998 9.49998C1.49998 11.7333 2.27498 13.625 3.82498 15.175C5.37498 16.725 7.26664 17.5 9.49998 17.5Z" fill="#FEFCC9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_21296_8285" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
|
||||||
|
<rect width="20" height="20" fill="#D9D9D9"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_21296_8285)">
|
||||||
|
<path d="M5.33327 15.5448L4.45508 14.6666L9.12174 9.99993L4.45508 5.33327L5.33327 4.45508L9.99993 9.12174L14.6666 4.45508L15.5448 5.33327L10.8781 9.99993L15.5448 14.6666L14.6666 15.5448L9.99993 10.8781L5.33327 15.5448Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 546 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6.92822 0L13.8564 7.5H1.95503e-05L6.92822 0Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 172 B |
@@ -0,0 +1,162 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import Alert from "../app/components/Alert";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Alert",
|
||||||
|
component: Alert,
|
||||||
|
argTypes: {
|
||||||
|
status: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["default", "positive", "warning", "danger"],
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["toast", "banner"],
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
control: { type: "text" },
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
control: { type: "text" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
if (!isVisible) return <div>Alert closed</div>;
|
||||||
|
return (
|
||||||
|
<div className="p-8 max-w-[600px]">
|
||||||
|
<Alert {...args} onClose={() => setIsVisible(false)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
Default.args = {
|
||||||
|
title: "Short alert banner message goes here",
|
||||||
|
description:
|
||||||
|
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
|
||||||
|
status: "default",
|
||||||
|
type: "toast",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Positive = Template.bind({});
|
||||||
|
Positive.args = {
|
||||||
|
title: "Short alert banner message goes here",
|
||||||
|
description:
|
||||||
|
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
|
||||||
|
status: "positive",
|
||||||
|
type: "toast",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Warning = Template.bind({});
|
||||||
|
Warning.args = {
|
||||||
|
title: "Short alert banner message goes here",
|
||||||
|
description:
|
||||||
|
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
|
||||||
|
status: "warning",
|
||||||
|
type: "toast",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Danger = Template.bind({});
|
||||||
|
Danger.args = {
|
||||||
|
title: "Short alert banner message goes here",
|
||||||
|
description:
|
||||||
|
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
|
||||||
|
status: "danger",
|
||||||
|
type: "toast",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Banner = Template.bind({});
|
||||||
|
Banner.args = {
|
||||||
|
title: "Short alert banner message goes here",
|
||||||
|
description:
|
||||||
|
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
|
||||||
|
status: "default",
|
||||||
|
type: "banner",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BannerPositive = Template.bind({});
|
||||||
|
BannerPositive.args = {
|
||||||
|
title: "Short alert banner message goes here",
|
||||||
|
description:
|
||||||
|
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
|
||||||
|
status: "positive",
|
||||||
|
type: "banner",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BannerWarning = Template.bind({});
|
||||||
|
BannerWarning.args = {
|
||||||
|
title: "Short alert banner message goes here",
|
||||||
|
description:
|
||||||
|
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
|
||||||
|
status: "warning",
|
||||||
|
type: "banner",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BannerDanger = Template.bind({});
|
||||||
|
BannerDanger.args = {
|
||||||
|
title: "Short alert banner message goes here",
|
||||||
|
description:
|
||||||
|
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
|
||||||
|
status: "danger",
|
||||||
|
type: "banner",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TitleOnly = Template.bind({});
|
||||||
|
TitleOnly.args = {
|
||||||
|
title: "Short alert banner message goes here",
|
||||||
|
status: "default",
|
||||||
|
type: "toast",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllStatuses = () => {
|
||||||
|
const [visible, setVisible] = useState({
|
||||||
|
default: true,
|
||||||
|
positive: true,
|
||||||
|
warning: true,
|
||||||
|
danger: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 space-y-4 max-w-[600px]">
|
||||||
|
{visible.default && (
|
||||||
|
<Alert
|
||||||
|
title="Default alert"
|
||||||
|
description="This is a default alert message"
|
||||||
|
status="default"
|
||||||
|
type="toast"
|
||||||
|
onClose={() => setVisible({ ...visible, default: false })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{visible.positive && (
|
||||||
|
<Alert
|
||||||
|
title="Positive alert"
|
||||||
|
description="This is a positive alert message"
|
||||||
|
status="positive"
|
||||||
|
type="toast"
|
||||||
|
onClose={() => setVisible({ ...visible, positive: false })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{visible.warning && (
|
||||||
|
<Alert
|
||||||
|
title="Warning alert"
|
||||||
|
description="This is a warning alert message"
|
||||||
|
status="warning"
|
||||||
|
type="toast"
|
||||||
|
onClose={() => setVisible({ ...visible, warning: false })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{visible.danger && (
|
||||||
|
<Alert
|
||||||
|
title="Danger alert"
|
||||||
|
description="This is a danger alert message"
|
||||||
|
status="danger"
|
||||||
|
type="toast"
|
||||||
|
onClose={() => setVisible({ ...visible, danger: false })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Tooltip from "../app/components/Tooltip";
|
||||||
|
import Button from "../app/components/Button";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Tooltip",
|
||||||
|
component: Tooltip,
|
||||||
|
argTypes: {
|
||||||
|
position: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["top", "bottom"],
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: { type: "boolean" },
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
control: { type: "text" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args) => (
|
||||||
|
<div className="p-16 flex items-center justify-center min-h-[200px]">
|
||||||
|
<Tooltip {...args}>
|
||||||
|
<Button variant="default" size="medium">
|
||||||
|
Hover me
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
||||||
|
Default.args = {
|
||||||
|
text: "Tooltip text goes here",
|
||||||
|
position: "top",
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Top = Template.bind({});
|
||||||
|
Top.args = {
|
||||||
|
text: "Tooltip positioned at top",
|
||||||
|
position: "top",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Bottom = Template.bind({});
|
||||||
|
Bottom.args = {
|
||||||
|
text: "Tooltip positioned at bottom",
|
||||||
|
position: "bottom",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled = Template.bind({});
|
||||||
|
Disabled.args = {
|
||||||
|
text: "This tooltip is disabled",
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LongText = Template.bind({});
|
||||||
|
LongText.args = {
|
||||||
|
text: "This is a longer tooltip text that demonstrates how the component handles multiple words and extended content",
|
||||||
|
position: "top",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithIcon = () => (
|
||||||
|
<div className="p-16 flex items-center justify-center min-h-[200px]">
|
||||||
|
<Tooltip text="Tooltip with icon button" position="top">
|
||||||
|
<button className="p-2 rounded-full hover:bg-[var(--color-surface-default-tertiary)] transition-colors">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 9V11M10 15H10.01M19 10C19 14.9706 14.9706 19 10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Alert from "../../app/components/Alert";
|
||||||
|
import { componentTestSuite } from "../utils/componentTestSuite";
|
||||||
|
|
||||||
|
type AlertProps = React.ComponentProps<typeof Alert>;
|
||||||
|
|
||||||
|
componentTestSuite<AlertProps>({
|
||||||
|
component: Alert,
|
||||||
|
name: "Alert",
|
||||||
|
props: {
|
||||||
|
title: "Alert title",
|
||||||
|
description: "Alert description",
|
||||||
|
} as AlertProps,
|
||||||
|
requiredProps: ["title"],
|
||||||
|
optionalProps: {
|
||||||
|
description: "Optional description",
|
||||||
|
status: "positive",
|
||||||
|
type: "banner",
|
||||||
|
},
|
||||||
|
primaryRole: "alert",
|
||||||
|
testCases: {
|
||||||
|
renders: true,
|
||||||
|
accessibility: true,
|
||||||
|
keyboardNavigation: false, // Alert is not directly keyboard navigable
|
||||||
|
disabledState: false,
|
||||||
|
errorState: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import Tooltip from "../../app/components/Tooltip";
|
||||||
|
import { componentTestSuite } from "../utils/componentTestSuite";
|
||||||
|
|
||||||
|
type TooltipProps = React.ComponentProps<typeof Tooltip>;
|
||||||
|
|
||||||
|
componentTestSuite<TooltipProps>({
|
||||||
|
component: Tooltip,
|
||||||
|
name: "Tooltip",
|
||||||
|
props: {
|
||||||
|
children: <button>Hover me</button>,
|
||||||
|
text: "Tooltip text",
|
||||||
|
} as TooltipProps,
|
||||||
|
requiredProps: ["children", "text"],
|
||||||
|
optionalProps: {
|
||||||
|
position: "bottom",
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
primaryRole: "button",
|
||||||
|
testCases: {
|
||||||
|
renders: true,
|
||||||
|
accessibility: true,
|
||||||
|
keyboardNavigation: true,
|
||||||
|
disabledState: false, // Tooltip disabled state is handled in behavioral tests
|
||||||
|
errorState: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Tooltip (behavioral tests)", () => {
|
||||||
|
it("shows tooltip on hover", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<Tooltip text="Tooltip text">
|
||||||
|
<button>Hover me</button>
|
||||||
|
</Tooltip>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = screen.getByRole("button", { name: "Hover me" });
|
||||||
|
await user.hover(button);
|
||||||
|
|
||||||
|
const tooltip = screen.getByRole("tooltip");
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(tooltip).toHaveTextContent("Tooltip text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides tooltip when disabled", () => {
|
||||||
|
render(
|
||||||
|
<Tooltip text="Tooltip text" disabled>
|
||||||
|
<button>Hover me</button>
|
||||||
|
</Tooltip>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tooltip = screen.queryByRole("tooltip");
|
||||||
|
expect(tooltip).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user