Skip to content

Commit 6e90670

Browse files
committed
refactor(alert): replace Alert.Dismiss with dismissible prop and reposition action below text
- Remove Alert.Dismiss compound component and useAlertDismiss hook - Add dismissible and onDismiss props to Alert.Root - Move action slot from right side (col-start-3) to below description (col-start-2) - Position dismiss button on the right side (col-start-3) for vertical space efficiency - Update examples and tests to use new API
1 parent 45118ca commit 6e90670

5 files changed

Lines changed: 52 additions & 75 deletions

File tree

examples/nextjs-app/src/modules/pages/primitives-demo.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,14 @@ export const primitiveComponentsDemoResource = defineResource({
188188
gap: "0.5rem",
189189
}}
190190
>
191-
<Alert.Root variant="info" action={<Alert.Dismiss />}>
191+
<Alert.Root variant="info" dismissible>
192192
<Alert.Title>Dismissible</Alert.Title>
193193
<Alert.Description>This alert can be dismissed.</Alert.Description>
194194
</Alert.Root>
195195
<Alert.Root
196196
variant="success"
197-
action={<Alert.Dismiss onDismiss={() => console.log("dismissed")} />}
197+
dismissible
198+
onDismiss={() => console.log("dismissed")}
198199
>
199200
<Alert.Title>With callback</Alert.Title>
200201
<Alert.Description>Dismiss fires a callback.</Alert.Description>

packages/core/__snapshots__/src__components__alert.test.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ exports[`Alert > snapshots > success variant 1`] = `"<div data-slot="alert" role
1010

1111
exports[`Alert > snapshots > warning variant 1`] = `"<div data-slot="alert" role="alert" class="astw:relative astw:w-full astw:rounded-lg astw:border astw:px-4 astw:py-3 astw:text-sm astw:grid astw:grid-cols-[calc(var(--spacing)*4.5)_1fr_auto] astw:gap-x-3 astw:gap-y-0.5 astw:items-start astw:[&amp;>svg]:size-[17.5px] astw:[&amp;>svg]:self-center astw:[&amp;>svg]:shrink-0 astw:[&amp;>svg]:text-current astw:bg-yellow-500/10 astw:text-yellow-700 astw:border-yellow-500/20 dark:astw:text-yellow-500 *:data-[slot=alert-description]:astw:text-yellow-700/80 dark:*:data-[slot=alert-description]:astw:text-yellow-500/80"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg><div data-slot="alert-title" class="astw:col-start-2 astw:line-clamp-1 astw:min-h-4 astw:font-medium astw:tracking-tight">Warning</div><div data-slot="alert-description" class="astw:col-start-2 astw:grid astw:justify-items-start astw:gap-1 astw:text-sm [&amp;_p]:astw:leading-relaxed">Be careful</div></div>"`;
1212

13-
exports[`Alert > snapshots > with action 1`] = `"<div data-slot="alert" role="alert" class="astw:relative astw:w-full astw:rounded-lg astw:border astw:px-4 astw:py-3 astw:text-sm astw:grid astw:grid-cols-[calc(var(--spacing)*4.5)_1fr_auto] astw:gap-x-3 astw:gap-y-0.5 astw:items-start astw:[&amp;>svg]:size-[17.5px] astw:[&amp;>svg]:self-center astw:[&amp;>svg]:shrink-0 astw:[&amp;>svg]:text-current astw:bg-secondary astw:text-secondary-foreground astw:border-border *:data-[slot=alert-description]:astw:text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg><div data-slot="alert-title" class="astw:col-start-2 astw:line-clamp-1 astw:min-h-4 astw:font-medium astw:tracking-tight">Dismissible</div><div data-slot="alert-description" class="astw:col-start-2 astw:grid astw:justify-items-start astw:gap-1 astw:text-sm [&amp;_p]:astw:leading-relaxed">This can be dismissed</div><div data-slot="alert-action" class="astw:col-start-3 astw:row-span-full astw:flex astw:items-center astw:self-center"><button data-slot="alert-dismiss" type="button" class="astw:inline-flex astw:items-center astw:justify-center astw:rounded-md astw:p-1 astw:opacity-70 astw:transition-opacity hover:astw:opacity-100 focus-visible:astw:outline-none focus-visible:astw:ring-2 focus-visible:astw:ring-ring" aria-label="Dismiss"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x astw:size-4" aria-hidden="true"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg></button></div></div>"`;
13+
exports[`Alert > snapshots > with dismissible 1`] = `"<div data-slot="alert" role="alert" class="astw:relative astw:w-full astw:rounded-lg astw:border astw:px-4 astw:py-3 astw:text-sm astw:grid astw:grid-cols-[calc(var(--spacing)*4.5)_1fr_auto] astw:gap-x-3 astw:gap-y-0.5 astw:items-start astw:[&amp;>svg]:size-[17.5px] astw:[&amp;>svg]:self-center astw:[&amp;>svg]:shrink-0 astw:[&amp;>svg]:text-current astw:bg-secondary astw:text-secondary-foreground astw:border-border *:data-[slot=alert-description]:astw:text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg><div data-slot="alert-title" class="astw:col-start-2 astw:line-clamp-1 astw:min-h-4 astw:font-medium astw:tracking-tight">Dismissible</div><div data-slot="alert-description" class="astw:col-start-2 astw:grid astw:justify-items-start astw:gap-1 astw:text-sm [&amp;_p]:astw:leading-relaxed">This can be dismissed</div><button data-slot="alert-dismiss" type="button" class="astw:col-start-3 astw:row-span-full astw:self-center astw:inline-flex astw:items-center astw:justify-center astw:rounded-md astw:p-1 astw:opacity-70 astw:transition-opacity hover:astw:opacity-100 focus-visible:astw:outline-none focus-visible:astw:ring-2 focus-visible:astw:ring-ring" aria-label="Dismiss"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x astw:size-4" aria-hidden="true"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg></button></div>"`;

packages/core/src/components/alert.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ describe("Alert", () => {
5959
expect(container.innerHTML).toMatchSnapshot();
6060
});
6161

62-
it("with action", () => {
62+
it("with dismissible", () => {
6363
const { container } = render(
64-
<Alert.Root action={<Alert.Dismiss />}>
64+
<Alert.Root dismissible>
6565
<Alert.Title>Dismissible</Alert.Title>
6666
<Alert.Description>This can be dismissed</Alert.Description>
6767
</Alert.Root>,
@@ -92,11 +92,11 @@ describe("Alert", () => {
9292
expect(alert.className).toContain("astw:mt-4");
9393
});
9494

95-
it("dismisses when Alert.Dismiss is clicked", async () => {
95+
it("dismisses when dismissible button is clicked", async () => {
9696
const user = userEvent.setup();
9797
const onDismiss = vi.fn();
9898
render(
99-
<Alert.Root action={<Alert.Dismiss onDismiss={onDismiss} />}>
99+
<Alert.Root dismissible onDismiss={onDismiss}>
100100
<Alert.Title>Dismissible</Alert.Title>
101101
</Alert.Root>,
102102
);

packages/core/src/components/alert.tsx

Lines changed: 43 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -43,43 +43,61 @@ const variantIcons: Record<
4343
info: InfoIcon,
4444
};
4545

46-
const AlertDismissContext = React.createContext<(() => void) | null>(null);
47-
4846
type RootProps = React.ComponentProps<"div"> &
4947
VariantProps<typeof alertVariants> & {
5048
action?: React.ReactNode;
49+
dismissible?: boolean;
50+
onDismiss?: () => void;
5151
};
5252

53-
function Root({ className, variant = "neutral", action, children, ...props }: RootProps) {
53+
function Root({
54+
className,
55+
variant = "neutral",
56+
action,
57+
dismissible,
58+
onDismiss,
59+
children,
60+
...props
61+
}: RootProps) {
5462
const [visible, setVisible] = React.useState(true);
5563
const Icon = variantIcons[variant ?? "neutral"];
5664

57-
const dismiss = React.useCallback(() => {
65+
const handleDismiss = React.useCallback(() => {
5866
setVisible(false);
59-
}, []);
67+
onDismiss?.();
68+
}, [onDismiss]);
6069

6170
if (!visible) return null;
6271

6372
return (
64-
<AlertDismissContext.Provider value={dismiss}>
65-
<div
66-
data-slot="alert"
67-
role="alert"
68-
className={cn(alertVariants({ variant }), className)}
69-
{...props}
70-
>
71-
<Icon />
72-
{children}
73-
{action && (
74-
<div
75-
data-slot="alert-action"
76-
className="astw:col-start-3 astw:row-span-full astw:flex astw:items-center astw:self-center"
77-
>
78-
{action}
79-
</div>
80-
)}
81-
</div>
82-
</AlertDismissContext.Provider>
73+
<div
74+
data-slot="alert"
75+
role="alert"
76+
className={cn(alertVariants({ variant }), className)}
77+
{...props}
78+
>
79+
<Icon />
80+
{children}
81+
{action && (
82+
<div
83+
data-slot="alert-action"
84+
className="astw:col-start-2 astw:flex astw:items-center astw:gap-2 astw:mt-2"
85+
>
86+
{action}
87+
</div>
88+
)}
89+
{dismissible && (
90+
<button
91+
data-slot="alert-dismiss"
92+
type="button"
93+
onClick={handleDismiss}
94+
className="astw:col-start-3 astw:row-span-full astw:self-center astw:inline-flex astw:items-center astw:justify-center astw:rounded-md astw:p-1 astw:opacity-70 astw:transition-opacity hover:astw:opacity-100 focus-visible:astw:outline-none focus-visible:astw:ring-2 focus-visible:astw:ring-ring"
95+
aria-label="Dismiss"
96+
>
97+
<XIcon className="astw:size-4" />
98+
</button>
99+
)}
100+
</div>
83101
);
84102
}
85103
Root.displayName = "Alert.Root";
@@ -112,48 +130,6 @@ function Description({ className, ...props }: React.ComponentProps<"div">) {
112130
}
113131
Description.displayName = "Alert.Description";
114132

115-
type DismissProps = Omit<React.ComponentProps<"button">, "onClick"> & {
116-
onDismiss?: () => void;
117-
};
118-
119-
function Dismiss({ className, onDismiss, ...props }: DismissProps) {
120-
const dismiss = React.useContext(AlertDismissContext);
121-
122-
const handleClick = React.useCallback(() => {
123-
dismiss?.();
124-
onDismiss?.();
125-
}, [dismiss, onDismiss]);
126-
127-
return (
128-
<button
129-
data-slot="alert-dismiss"
130-
type="button"
131-
onClick={handleClick}
132-
className={cn(
133-
"astw:inline-flex astw:items-center astw:justify-center astw:rounded-md astw:p-1 astw:opacity-70 astw:transition-opacity hover:astw:opacity-100 focus-visible:astw:outline-none focus-visible:astw:ring-2 focus-visible:astw:ring-ring",
134-
className,
135-
)}
136-
aria-label="Dismiss"
137-
{...props}
138-
>
139-
<XIcon className="astw:size-4" />
140-
</button>
141-
);
142-
}
143-
Dismiss.displayName = "Alert.Dismiss";
144-
145-
/**
146-
* Returns the dismiss function for the nearest `Alert.Root`.
147-
* @throws {Error} if called outside an `Alert.Root` tree.
148-
*/
149-
export function useAlertDismiss() {
150-
const dismiss = React.useContext(AlertDismissContext);
151-
if (!dismiss) {
152-
throw new Error("useAlertDismiss must be used within an Alert.Root");
153-
}
154-
return dismiss;
155-
}
156-
157133
export type AlertProps = RootProps;
158-
export const Alert = { Root, Title, Description, Dismiss };
134+
export const Alert = { Root, Title, Description };
159135
export { alertVariants };

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export { useToast } from "./hooks/use-toast";
8383
export { useOverrideBreadcrumb } from "./hooks/use-override-breadcrumb";
8484

8585
// Components
86-
export { Alert, alertVariants, useAlertDismiss, type AlertProps } from "./components/alert";
86+
export { Alert, alertVariants, type AlertProps } from "./components/alert";
8787
export { Badge, badgeVariants, type BadgeProps } from "./components/badge";
8888
export type { BadgeVariant, BadgeOptions } from "./components/badge-list";
8989
export { DescriptionCard, type DescriptionCardProps } from "./components/description-card";

0 commit comments

Comments
 (0)