Skip to content

Commit 408f0f6

Browse files
committed
feat(callout): Add Callout Components
1 parent a5ff1f8 commit 408f0f6

File tree

3 files changed

+187
-1
lines changed

3 files changed

+187
-1
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { Callout } from './Container';
3+
4+
describe('Callout Component', () => {
5+
describe('rendering', () => {
6+
it('should be visible', () => {
7+
render(<Callout>Test content</Callout>);
8+
expect(screen.getByText('Test content')).toBeVisible();
9+
});
10+
11+
it('should render children correctly', () => {
12+
const testText = 'This is a test callout message';
13+
render(<Callout>{testText}</Callout>);
14+
expect(screen.getByText(testText)).toBeInTheDocument();
15+
});
16+
17+
it('should render complex children', () => {
18+
render(
19+
<Callout>
20+
<div>
21+
<span>Complex</span> content with <strong>HTML</strong>
22+
</div>
23+
</Callout>,
24+
);
25+
expect(screen.getByText('Complex')).toBeInTheDocument();
26+
expect(screen.getByText('HTML')).toBeInTheDocument();
27+
});
28+
});
29+
30+
describe('info callout', () => {
31+
it('should display info icon when info prop is true', () => {
32+
render(<Callout info>Info message</Callout>);
33+
expect(screen.getByText('ℹ️')).toBeInTheDocument();
34+
});
35+
36+
it('should have correct info styling', () => {
37+
const { container } = render(<Callout info>Info message</Callout>);
38+
const calloutDiv = container.firstChild as HTMLElement;
39+
expect(calloutDiv).toHaveClass('bg-blue-100', 'text-blue-800');
40+
});
41+
});
42+
43+
describe('warning callout', () => {
44+
it('should display warning icon when warning prop is true', () => {
45+
render(<Callout warning>Warning message</Callout>);
46+
expect(screen.getByText('⚠️')).toBeInTheDocument();
47+
});
48+
49+
it('should have correct warning styling', () => {
50+
const { container } = render(<Callout warning>Warning message</Callout>);
51+
const calloutDiv = container.firstChild as HTMLElement;
52+
expect(calloutDiv).toHaveClass('bg-yellow-100', 'text-yellow-800');
53+
});
54+
});
55+
56+
describe('error callout', () => {
57+
it('should display error icon when error prop is true', () => {
58+
render(<Callout error>Error message</Callout>);
59+
expect(screen.getByText('❗')).toBeInTheDocument();
60+
});
61+
62+
it('should have correct error styling', () => {
63+
const { container } = render(<Callout error>Error message</Callout>);
64+
const calloutDiv = container.firstChild as HTMLElement;
65+
expect(calloutDiv).toHaveClass('bg-red-100', 'text-red-800');
66+
});
67+
68+
it('should default to error when no type is specified', () => {
69+
render(<Callout>Default message</Callout>);
70+
expect(screen.getByText('❗')).toBeInTheDocument();
71+
});
72+
});
73+
74+
describe('type priority', () => {
75+
it('should prioritize info over warning and error', () => {
76+
render(
77+
<Callout info warning error>
78+
Priority test
79+
</Callout>,
80+
);
81+
expect(screen.getByText('ℹ️')).toBeInTheDocument();
82+
expect(screen.queryByText('⚠️')).not.toBeInTheDocument();
83+
expect(screen.queryByText('❗')).not.toBeInTheDocument();
84+
});
85+
86+
it('should prioritize warning over error when info is false', () => {
87+
render(
88+
<Callout warning error>
89+
Priority test
90+
</Callout>,
91+
);
92+
expect(screen.getByText('⚠️')).toBeInTheDocument();
93+
expect(screen.queryByText('❗')).not.toBeInTheDocument();
94+
});
95+
});
96+
97+
describe('styling and classes', () => {
98+
it('should have base classes', () => {
99+
const { container } = render(<Callout>Test</Callout>);
100+
const calloutDiv = container.firstChild as HTMLElement;
101+
expect(calloutDiv).toHaveClass(
102+
'flex',
103+
'flex-row',
104+
'gap-1.75',
105+
'px-4',
106+
'py-1',
107+
'rounded-lg',
108+
);
109+
});
110+
111+
it('should have correct text styling for content', () => {
112+
render(<Callout>Test content</Callout>);
113+
const contentDiv = screen.getByText('Test content');
114+
expect(contentDiv).toHaveClass('text-[0.95rem]', 'pr-1');
115+
});
116+
117+
it('should have correct icon styling', () => {
118+
render(<Callout info>Test</Callout>);
119+
const iconDiv = screen.getByText('ℹ️');
120+
expect(iconDiv).toHaveClass('text-2xl', 'pt-3.5');
121+
});
122+
});
123+
124+
describe('accessibility', () => {
125+
it('should be accessible with proper structure', () => {
126+
render(<Callout info>Accessible content</Callout>);
127+
const content = screen.getByText('Accessible content');
128+
expect(content).toBeInTheDocument();
129+
expect(content.closest('div')).toBeInTheDocument();
130+
});
131+
132+
it('should maintain semantic structure for screen readers', () => {
133+
const { container } = render(
134+
<Callout warning>Important warning message</Callout>,
135+
);
136+
expect(container.firstChild).toHaveAttribute('class');
137+
expect(screen.getByText('Important warning message')).toBeInTheDocument();
138+
expect(screen.getByText('⚠️')).toBeInTheDocument();
139+
});
140+
});
141+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import classNames from 'classnames';
2+
import type { FunctionComponent } from 'react';
3+
4+
type CalloutType = 'info' | 'warning' | 'error';
5+
type CalloutValue = 'ℹ️' | '⚠️' | '❗';
6+
7+
export const Callout = ({
8+
children,
9+
info = false,
10+
warning = false,
11+
error = false,
12+
}: {
13+
children: React.ReactNode;
14+
info?: boolean;
15+
warning?: boolean;
16+
error?: boolean;
17+
}) => {
18+
const type: CalloutType = info ? 'info' : warning ? 'warning' : 'error';
19+
return (
20+
<div
21+
className={classNames('flex flex-row gap-1.75 px-4 py-1 rounded-lg', {
22+
'bg-blue-100 text-blue-800': info,
23+
'bg-yellow-100 text-yellow-800': warning,
24+
'bg-red-100 text-red-800': error,
25+
})}
26+
>
27+
<Type type={type} />
28+
<div className="text-[0.95rem] pr-1">{children}</div>
29+
</div>
30+
);
31+
};
32+
33+
const Type: FunctionComponent<{ type: CalloutType }> = ({ type }) => {
34+
const typeMap: Record<CalloutType, CalloutValue> = {
35+
info: 'ℹ️',
36+
warning: '⚠️',
37+
error: '❗',
38+
};
39+
return <div className="text-2xl pt-3.5">{typeMap[type]}</div>;
40+
};

src/mdx-components.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { MDXComponents } from 'mdx/types';
2+
import { Callout } from '~/shared/components/Callout/Container';
23
import { HeadingFactory } from '~/shared/components/mdx/Headings';
34
import {
45
ImageTag,
@@ -7,8 +8,11 @@ import {
78
import { BasicTable } from '~/shared/components/mdx/table/BasicTable';
89

910
export function useMDXComponents(components: MDXComponents): MDXComponents {
10-
return {
11+
const parser = {
1112
...components,
13+
Callout: (props) => {
14+
return <Callout {...props}>{parser.p(props)}</Callout>;
15+
},
1216
img: (props: ImageTagProps) => <ImageTag {...props} />,
1317
blockquote: (props) => {
1418
// > ... 참조
@@ -28,4 +32,5 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
2832
table: (props) => <BasicTable {...props} />,
2933
...HeadingFactory(),
3034
};
35+
return parser;
3136
}

0 commit comments

Comments
 (0)