Skip to content

Commit 0c0b27c

Browse files
authored
AlertDialog Test coverage improvement (#1508)
1 parent 2301f04 commit 0c0b27c

19 files changed

+1648
-172
lines changed

src/components/ui/AlertDialog/AlertDialog.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import AlertDialogCancel from './fragments/AlertDialogCancel';
99
import AlertDialogAction from './fragments/AlertDialogAction';
1010
import AlertDialogTitle from './fragments/AlertDialogTitle';
1111
import AlertDialogDescription from './fragments/AlertDialogDescription';
12+
// Explicit extension to satisfy ESM linting/resolution
13+
export type {
14+
AlertDialogRootProps as __fix_types_1
15+
} from './types.ts';
1216

1317
const AlertDialog = () => {
1418
console.warn('Direct usage of AlertDialog is not supported. Please use AlertDialog.Root, AlertDialog.Content, etc. instead.');
Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
'use client';
22
import { createContext } from 'react';
33

4-
type AlertDialogContextType = {
4+
export type AlertDialogContextType = {
55
rootClass: string;
6+
isOpen: boolean;
7+
setIsOpen: (open: boolean) => void;
8+
titleId?: string;
9+
descriptionId?: string;
10+
setTitleId: (id: string | undefined) => void;
11+
setDescriptionId: (id: string | undefined) => void;
612
};
713

814
export const AlertDialogContext = createContext<AlertDialogContextType>({
9-
rootClass: ''
15+
rootClass: '',
16+
isOpen: false,
17+
setIsOpen: () => {},
18+
titleId: undefined,
19+
descriptionId: undefined,
20+
setTitleId: () => {},
21+
setDescriptionId: () => {}
1022
});

src/components/ui/AlertDialog/fragments/AlertDialogContent.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,30 @@ type DialogPrimitiveContentProps = React.ComponentPropsWithoutRef<typeof DialogP
99

1010
export type AlertDialogContentProps = DialogPrimitiveContentProps & {
1111
className?: string;
12+
asChild?: boolean;
13+
forceMount?: boolean;
1214
};
1315

14-
const AlertDialogContent = forwardRef<AlertDialogContentElement, AlertDialogContentProps>(({ children, className = '', ...props }, ref) => {
15-
const { rootClass } = useContext(AlertDialogContext);
16+
const AlertDialogContent = forwardRef<AlertDialogContentElement, AlertDialogContentProps>(({
17+
children,
18+
className = '',
19+
asChild = false,
20+
forceMount = false,
21+
...props
22+
}, ref) => {
23+
const { rootClass, titleId, descriptionId } = useContext(AlertDialogContext);
1624
return (
17-
<DialogPrimitive.Content ref={ref} className={clsx(`${rootClass}-content`, className)} {...props}>
25+
<DialogPrimitive.Content
26+
ref={ref}
27+
className={clsx(`${rootClass}-content`, className)}
28+
asChild={asChild}
29+
forceMount={forceMount}
30+
role="alertdialog"
31+
aria-modal={true}
32+
aria-labelledby={titleId}
33+
aria-describedby={descriptionId}
34+
{...props}
35+
>
1836
{children}
1937
</DialogPrimitive.Content>
2038
);

src/components/ui/AlertDialog/fragments/AlertDialogDescription.tsx

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,63 @@
11
'use client';
22

3-
import React, { forwardRef, useContext } from 'react';
3+
import React, { forwardRef, useContext, useEffect, useRef } from 'react';
44
import { AlertDialogContext } from '../contexts/AlertDialogContext';
5+
import Floater from '~/core/primitives/Floater';
56

67
import Primitive from '~/core/primitives/Primitive';
78

89
type AlertDialogDescriptionElement = React.ElementRef<typeof Primitive.p>;
910
type PrimitivePProps = React.ComponentPropsWithoutRef<typeof Primitive.p>;
1011

11-
export type AlertDialogDescriptionProps = PrimitivePProps & { className?: string };
12+
export type AlertDialogDescriptionProps = PrimitivePProps & {
13+
className?: string;
14+
asChild?: boolean;
15+
};
1216

13-
const AlertDialogDescription = forwardRef<AlertDialogDescriptionElement, AlertDialogDescriptionProps>(({ children, className = '', ...props }, ref) => {
14-
const { rootClass } = useContext(AlertDialogContext);
15-
return <Primitive.p ref={ref} className={`${rootClass}-description ${className}`} {...props}>{children}</Primitive.p>;
17+
const AlertDialogDescription = forwardRef<AlertDialogDescriptionElement, AlertDialogDescriptionProps>(({
18+
children,
19+
className = '',
20+
asChild = false,
21+
id,
22+
...props
23+
}, ref) => {
24+
const { rootClass, setDescriptionId, descriptionId: currentDescriptionId } = useContext(AlertDialogContext);
25+
const generatedId = Floater.useId();
26+
const descriptionId = id ?? generatedId;
27+
const descriptionIdRef = useRef(descriptionId);
28+
const latestCurrentDescriptionIdRef = useRef(currentDescriptionId);
29+
30+
// Update the latest current description ID ref whenever it changes
31+
useEffect(() => {
32+
latestCurrentDescriptionIdRef.current = currentDescriptionId;
33+
}, [currentDescriptionId]);
34+
35+
useEffect(() => {
36+
descriptionIdRef.current = descriptionId;
37+
if (descriptionId) {
38+
setDescriptionId(descriptionId);
39+
}
40+
41+
// Cleanup: clear the descriptionId when this component unmounts
42+
// Only clear if the stored id still matches to avoid clobbering other instances
43+
return () => {
44+
if (latestCurrentDescriptionIdRef.current === descriptionIdRef.current) {
45+
setDescriptionId(undefined);
46+
}
47+
};
48+
}, [descriptionId, setDescriptionId]);
49+
50+
return (
51+
<Primitive.p
52+
ref={ref}
53+
id={descriptionId}
54+
className={`${rootClass}-description ${className}`}
55+
asChild={asChild}
56+
{...props}
57+
>
58+
{children}
59+
</Primitive.p>
60+
);
1661
});
1762

1863
AlertDialogDescription.displayName = 'AlertDialogDescription';

src/components/ui/AlertDialog/fragments/AlertDialogOverlay.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,31 @@ import DialogPrimitive from '~/core/primitives/Dialog';
88
type AlertDialogOverlayElement = React.ElementRef<typeof DialogPrimitive.Overlay>;
99
type DialogPrimitiveOverlayProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>;
1010

11-
type AlertDialogOverlayProps = DialogPrimitiveOverlayProps & {
11+
export type AlertDialogOverlayProps = DialogPrimitiveOverlayProps & {
1212
className?: string;
13+
asChild?: boolean;
14+
forceMount?: boolean;
15+
children?: React.ReactNode;
1316
};
1417

15-
const AlertDialogOverlay = forwardRef<AlertDialogOverlayElement, AlertDialogOverlayProps>(({ className = '', ...props }, ref) => {
18+
const AlertDialogOverlay = forwardRef<AlertDialogOverlayElement, AlertDialogOverlayProps>(({
19+
className = '',
20+
asChild = false,
21+
forceMount = false,
22+
children,
23+
...props
24+
}, ref) => {
1625
const { rootClass } = useContext(AlertDialogContext);
1726
return (
18-
<DialogPrimitive.Overlay ref={ref} className={clsx(`${rootClass}-overlay`, className)} {...props}></DialogPrimitive.Overlay>
27+
<DialogPrimitive.Overlay
28+
ref={ref}
29+
className={clsx(`${rootClass}-overlay`, className)}
30+
asChild={asChild}
31+
forceMount={forceMount}
32+
{...props}
33+
>
34+
{children}
35+
</DialogPrimitive.Overlay>
1936
);
2037
});
2138

src/components/ui/AlertDialog/fragments/AlertDialogPortal.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
'use client';
2-
import React, { forwardRef } from 'react';
2+
import React from 'react';
33
import DialogPrimitive from '~/core/primitives/Dialog';
44

5-
type AlertDialogPortalElement = React.ElementRef<'div'>;
65
type DialogPrimitivePortalProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Portal>;
76

8-
export type AlertDialogPortalProps = DialogPrimitivePortalProps;
7+
export type AlertDialogPortalProps = DialogPrimitivePortalProps & {
8+
container?: Element | null;
9+
forceMount?: boolean;
10+
keepMounted?: boolean;
11+
};
912

10-
const AlertDialogPortal = forwardRef<AlertDialogPortalElement, AlertDialogPortalProps>(({ children, ...props }, _ref) => {
13+
const AlertDialogPortal = ({
14+
children,
15+
...props
16+
}: AlertDialogPortalProps) => {
1117
return (
12-
<DialogPrimitive.Portal {...props}>
18+
<DialogPrimitive.Portal
19+
{...props}
20+
>
1321
{children}
1422
</DialogPrimitive.Portal>
1523
);
16-
});
24+
};
1725

1826
AlertDialogPortal.displayName = 'AlertDialogPortal';
1927

src/components/ui/AlertDialog/fragments/AlertDialogRoot.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,52 @@
11
'use client';
2-
import React, { forwardRef } from 'react';
2+
import React, { forwardRef, useState } from 'react';
33
import { customClassSwitcher } from '~/core';
44
import { AlertDialogContext } from '../contexts/AlertDialogContext';
55
import { clsx } from 'clsx';
6+
import { useControllableState } from '~/core/hooks/useControllableState';
67

78
import DialogPrimitive from '~/core/primitives/Dialog';
89

910
type AlertDialogRootElement = React.ElementRef<typeof DialogPrimitive.Root>;
1011
type DialogPrimitiveRootProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root>;
1112

12-
export type AlertDialogRootProps = DialogPrimitiveRootProps & {
13+
export type AlertDialogRootProps = Omit<DialogPrimitiveRootProps, 'open' | 'onOpenChange'> & {
1314
customRootClass?: string;
1415
className?: string;
16+
defaultOpen?: boolean;
17+
open?: boolean;
18+
onOpenChange?: (open: boolean) => void;
1519
};
1620

1721
const COMPONENT_NAME = 'AlertDialog';
1822

19-
const AlertDialogRoot = forwardRef<AlertDialogRootElement, AlertDialogRootProps>(({ children, className = '', customRootClass = '', open = false, ...props }, ref) => {
23+
const AlertDialogRoot = forwardRef<AlertDialogRootElement, AlertDialogRootProps>(({
24+
children,
25+
className = '',
26+
customRootClass = '',
27+
defaultOpen = false,
28+
open,
29+
onOpenChange,
30+
...props
31+
}, ref) => {
2032
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
2133

22-
const contextProps = { rootClass };
34+
const [isOpen, setIsOpen] = useControllableState(open, defaultOpen, onOpenChange);
35+
const [titleId, setTitleId] = useState<string | undefined>(undefined);
36+
const [descriptionId, setDescriptionId] = useState<string | undefined>(undefined);
37+
38+
const contextProps = {
39+
rootClass,
40+
isOpen,
41+
setIsOpen,
42+
titleId,
43+
descriptionId,
44+
setTitleId,
45+
setDescriptionId
46+
};
47+
2348
return (
24-
<DialogPrimitive.Root open={open} className={clsx(rootClass, className)} {...props}>
49+
<DialogPrimitive.Root open={isOpen} onOpenChange={setIsOpen} className={clsx(rootClass, className)} {...props}>
2550
<AlertDialogContext.Provider value={contextProps}>
2651
<div ref={ref} className={clsx(rootClass, className)}>
2752
{children}

src/components/ui/AlertDialog/fragments/AlertDialogTitle.tsx

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22

3-
import React, { forwardRef, useContext } from 'react';
3+
import React, { forwardRef, useContext, useEffect, useRef } from 'react';
44
import { AlertDialogContext } from '../contexts/AlertDialogContext';
5+
import Floater from '~/core/primitives/Floater';
56

67
import Primitive from '~/core/primitives/Primitive';
78

@@ -10,11 +11,53 @@ type PrimitiveH2Props = React.ComponentPropsWithoutRef<typeof Primitive.h2>;
1011

1112
export type AlertDialogTitleProps = PrimitiveH2Props & {
1213
className?: string;
14+
asChild?: boolean;
1315
};
1416

15-
const AlertDialogTitle = forwardRef<AlertDialogTitleElement, AlertDialogTitleProps>(({ children, className = '', ...props }, ref) => {
16-
const { rootClass } = useContext(AlertDialogContext);
17-
return <Primitive.h2 ref={ref} className={`${rootClass}-title ${className}`} {...props}>{children}</Primitive.h2>;
17+
const AlertDialogTitle = forwardRef<AlertDialogTitleElement, AlertDialogTitleProps>(({
18+
children,
19+
className = '',
20+
asChild = false,
21+
id,
22+
...props
23+
}, ref) => {
24+
const { rootClass, setTitleId, titleId: currentTitleId } = useContext(AlertDialogContext);
25+
const generatedId = Floater.useId();
26+
const titleId = id ?? generatedId;
27+
const titleIdRef = useRef(titleId);
28+
const latestCurrentTitleIdRef = useRef(currentTitleId);
29+
30+
// Update the latest current title ID ref whenever it changes
31+
useEffect(() => {
32+
latestCurrentTitleIdRef.current = currentTitleId;
33+
}, [currentTitleId]);
34+
35+
useEffect(() => {
36+
titleIdRef.current = titleId;
37+
if (titleId) {
38+
setTitleId(titleId);
39+
}
40+
41+
// Cleanup: clear the titleId when this component unmounts
42+
// Only clear if the stored id still matches to avoid clobbering other instances
43+
return () => {
44+
if (latestCurrentTitleIdRef.current === titleIdRef.current) {
45+
setTitleId(undefined);
46+
}
47+
};
48+
}, [titleId, setTitleId]);
49+
50+
return (
51+
<Primitive.h2
52+
ref={ref}
53+
id={titleId}
54+
className={`${rootClass}-title ${className}`}
55+
asChild={asChild}
56+
{...props}
57+
>
58+
{children}
59+
</Primitive.h2>
60+
);
1861
});
1962

2063
AlertDialogTitle.displayName = 'AlertDialogTitle';

src/components/ui/AlertDialog/fragments/AlertDialogTrigger.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,33 @@ type DialogPrimitiveTriggerProps = React.ComponentPropsWithoutRef<typeof DialogP
1010

1111
export type AlertDialogTriggerProps = DialogPrimitiveTriggerProps & {
1212
className?: string;
13+
disabled?: boolean;
1314
};
1415

15-
const AlertDialogTrigger = forwardRef<AlertDialogTriggerElement, AlertDialogTriggerProps>(({ children, asChild, className = '', ...props }, ref) => {
16-
const { rootClass } = useContext(AlertDialogContext);
16+
const AlertDialogTrigger = forwardRef<AlertDialogTriggerElement, AlertDialogTriggerProps>(({
17+
children,
18+
asChild,
19+
className = '',
20+
disabled = false,
21+
...props
22+
}, ref) => {
23+
const { rootClass, isOpen } = useContext(AlertDialogContext);
24+
25+
const dataState = isOpen ? 'open' : 'closed';
26+
const dataDisabled = disabled ? '' : undefined;
1727

1828
return (
19-
<DialogPrimitive.Trigger ref={ref} className={clsx(`${rootClass}-trigger`, className)} asChild={asChild} {...props}>
29+
<DialogPrimitive.Trigger
30+
ref={ref}
31+
className={clsx(`${rootClass}-trigger`, className)}
32+
asChild={asChild}
33+
disabled={disabled}
34+
data-state={dataState}
35+
data-disabled={dataDisabled}
36+
{...props}
37+
>
2038
{children}
2139
</DialogPrimitive.Trigger>
22-
2340
);
2441
});
2542

0 commit comments

Comments
 (0)