Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,84 @@
exports[`XMarkdown animation parent is custom components 1`] = `
<div>
<div
class="ant-x-markdown x-markdown"
class="ant-x-markdown-container ant-x-markdown x-markdown"
style="direction: ltr;"
>
<p>
This is Text.
</p>
<div
class="ant-x-markdown-content"
>
<p>
This is Text.
</p>
</div>
</div>
</div>
`;

exports[`XMarkdown animation parent is not custom components 1`] = `
<div>
<div
class="ant-x-markdown x-markdown"
class="ant-x-markdown-container ant-x-markdown x-markdown"
style="direction: ltr;"
>
<p>
<span
style="animation: x-markdown-fade-in 200ms ease-in-out forwards;"
>
This is Text.
</span>
</p>
<div
class="ant-x-markdown-content"
>
<p>
<span
style="animation: x-markdown-fade-in 200ms ease-in-out forwards;"
>
This is Text.
</span>
</p>
</div>
</div>
</div>
`;

exports[`XMarkdown support checkbox is checked 1`] = `
<div>
<div
class="ant-x-markdown x-markdown"
class="ant-x-markdown-container ant-x-markdown x-markdown"
style="direction: ltr;"
>
<ul>
<li>
<input
checked=""
disabled=""
type="checkbox"
/>
checkbox
</li>
</ul>
<div
class="ant-x-markdown-content"
>
<ul>
<li>
<input
checked=""
disabled=""
type="checkbox"
/>
checkbox
</li>
</ul>
</div>
</div>
</div>
`;

exports[`XMarkdown support checkbox not checked 1`] = `
<div>
<div
class="ant-x-markdown x-markdown"
class="ant-x-markdown-container ant-x-markdown x-markdown"
style="direction: ltr;"
>
<ul>
<li>
<input
disabled=""
type="checkbox"
/>
checkbox
</li>
</ul>
<div
class="ant-x-markdown-content"
>
<ul>
<li>
<input
disabled=""
type="checkbox"
/>
checkbox
</li>
</ul>
</div>
</div>
</div>
`;
141 changes: 141 additions & 0 deletions packages/x-markdown/src/XMarkdown/__test__/footer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import XMarkdown from '../index';

describe('XMarkdown Footer', () => {
it('should render footer when provided', () => {
const footerContent = <div data-testid="custom-footer">Loading...</div>;
render(<XMarkdown content="# Hello World" footer={footerContent} />);

expect(screen.getByTestId('custom-footer')).toBeInTheDocument();
expect(screen.getByTestId('custom-footer')).toHaveTextContent('Loading...');
});

it('should render footer at the end of content', () => {
const footerContent = <div data-testid="footer">Footer Content</div>;
const { container } = render(
<XMarkdown content="# Hello World\n\nThis is content." footer={footerContent} />,
);

const markdownContainer = container.firstChild;
expect(markdownContainer).toHaveClass('ant-x-markdown-container');

const content = container.querySelector('.ant-x-markdown-content');
const footer = container.querySelector('.ant-x-markdown-footer');

expect(content).toBeInTheDocument();
expect(footer).toBeInTheDocument();
expect(footer).toContainElement(screen.getByTestId('footer'));
});

it('should not render footer when not provided', () => {
const { container } = render(<XMarkdown content="# Hello World" />);

expect(container.querySelector('.ant-x-markdown-footer')).not.toBeInTheDocument();
});

it('should render footer with empty content', () => {
const footerContent = <div data-testid="empty-footer">Empty Content Footer</div>;
render(<XMarkdown content="" footer={footerContent} />);

expect(screen.getByTestId('empty-footer')).toBeInTheDocument();
});

it('should render footer with null content', () => {
const footerContent = <div data-testid="null-footer">Null Content Footer</div>;
render(<XMarkdown content={null as any} footer={footerContent} />);

expect(screen.getByTestId('null-footer')).toBeInTheDocument();
});

it('should render footer with undefined content', () => {
const footerContent = <div data-testid="undefined-footer">Undefined Content Footer</div>;
render(<XMarkdown content={undefined} footer={footerContent} />);

expect(screen.getByTestId('undefined-footer')).toBeInTheDocument();
});

it('should render footer with streaming content', () => {
const footerContent = <div data-testid="streaming-footer">Streaming...</div>;
const { rerender } = render(
<XMarkdown content="# Hello" footer={footerContent} streaming={{ hasNextChunk: true }} />,
);

expect(screen.getByTestId('streaming-footer')).toBeInTheDocument();
expect(screen.getByTestId('streaming-footer')).toHaveTextContent('Streaming...');

// Update content to simulate streaming
rerender(
<XMarkdown
content="# Hello World\n\nUpdated content"
footer={footerContent}
streaming={{ hasNextChunk: true }}
/>,
);

// Footer should still be visible
expect(screen.getByTestId('streaming-footer')).toBeInTheDocument();
});

it('should render footer with complex React components', () => {
const ComplexFooter = () => (
<div>
<span>Loading</span>
<div className="spinner">⚡</div>
</div>
);

render(<XMarkdown content="# Complex Content" footer={<ComplexFooter />} />);

expect(screen.getByText('Loading')).toBeInTheDocument();
expect(screen.getByText('⚡')).toBeInTheDocument();
});

it('should apply custom styles to footer container', () => {
const footerContent = <div style={{ color: 'red' }}>Styled Footer</div>;
const { container } = render(<XMarkdown content="# Test" footer={footerContent} />);

const footer = container.querySelector('.ant-x-markdown-footer');
expect(footer).toBeInTheDocument();
// Check CSS classes instead of computed styles
expect(footer).toHaveClass('ant-x-markdown-footer');
});

it('should work with custom prefixCls', () => {
const footerContent = <div data-testid="custom-prefix-footer">Custom Prefix</div>;
const { container } = render(
<XMarkdown content="# Test" footer={footerContent} prefixCls="custom-markdown" />,
);

expect(container.querySelector('.custom-markdown-container')).toBeInTheDocument();
expect(container.querySelector('.custom-markdown-content')).toBeInTheDocument();
expect(container.querySelector('.custom-markdown-footer')).toBeInTheDocument();
});

it('should not render anything when both content and footer are empty', () => {
const { container } = render(<XMarkdown content="" footer={null} />);
expect(container).toBeEmptyDOMElement();
});

it('should render footer with string content', () => {
render(<XMarkdown footer={<div>String Footer</div>}>{'# String Content'}</XMarkdown>);

expect(screen.getByText('String Footer')).toBeInTheDocument();
expect(screen.getByText('String Content')).toBeInTheDocument();
});

it('should memoize footer rendering for performance', () => {
const FooterComponent = jest.fn(() => <div>Memoized Footer</div>);
const footerElement = <FooterComponent />;

const { rerender } = render(<XMarkdown content="# Test" footer={footerElement} />);

expect(FooterComponent).toHaveBeenCalledTimes(1);

// Re-render with same footer element reference
rerender(<XMarkdown content="# Test Updated" footer={footerElement} />);

// Footer component should not re-render due to memoization
expect(FooterComponent).toHaveBeenCalledTimes(1);
});
});
6 changes: 3 additions & 3 deletions packages/x-markdown/src/XMarkdown/__test__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe('XMarkdown', () => {
it(`common markdown case: ${title}`, () => {
const { container } = render(<XMarkdown content={markdown} />);

expect((container.firstChild as HTMLElement)?.innerHTML).toBe(html);
expect((container.firstChild as HTMLElement)?.innerHTML).toContain(html);
});
});

Expand All @@ -162,7 +162,7 @@ describe('XMarkdown', () => {
/>,
);

expect((container.firstChild as HTMLElement)?.innerHTML).toBe(html);
expect((container.firstChild as HTMLElement)?.innerHTML).toContain(html);
});

it('walkToken', () => {
Expand Down Expand Up @@ -195,7 +195,7 @@ describe('XMarkdown', () => {
// XMarkdown wraps content in a div with class "ant-x-markdown"
const wrapper = container.firstChild as HTMLElement;
expect(wrapper).toHaveClass('ant-x-markdown');
expect(wrapper.innerHTML).toBe('<div>This is a paragraph.</div>\n');
expect(wrapper.innerHTML).toContain('<div>This is a paragraph.</div>\n');
});

it('support checkbox is checked', () => {
Expand Down
19 changes: 19 additions & 0 deletions packages/x-markdown/src/XMarkdown/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,22 @@
.x-markdown .inline-katex .katex-display > .katex > .katex-html > .tag {
position: static;
}

/* ==========================================
XMarkdown – footer css
========================================== */

.x-markdown-container {
position: relative;
width: 100%;
}

.x-markdown-content {
width: 100%;
}

.x-markdown-footer {
margin-top: 16px;
width: 100%;
/* 用户可完全自定义样式 */
}
34 changes: 31 additions & 3 deletions packages/x-markdown/src/XMarkdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const XMarkdown: React.FC<XMarkdownProps> = React.memo((props) => {
style,
openLinksInNewTab,
dompurifyConfig,
footer,
footerClassName,
footerStyle,
} = props;

// ============================ style ============================
Expand All @@ -37,6 +40,14 @@ const XMarkdown: React.FC<XMarkdownProps> = React.memo((props) => {
[contextDirection, style],
);

const mergedFooterStyle: React.CSSProperties = useMemo(
() => ({
direction: contextDirection === 'rtl' ? 'rtl' : 'ltr',
...footerStyle,
}),
[contextDirection, footerStyle],
);

// ============================ Streaming ============================
const displayContent = useStreaming(content || children || '', streaming);

Expand Down Expand Up @@ -66,11 +77,28 @@ const XMarkdown: React.FC<XMarkdownProps> = React.memo((props) => {
return parser.parse(displayContent);
}, [displayContent, parser]);

if (!displayContent) return null;
// Memoize footer rendering to prevent unnecessary re-renders
const footerElement = useMemo(() => {
if (!footer) return null;
return (
<div
className={classnames(`${prefixCls}-footer`, footerClassName)}
style={mergedFooterStyle}
key="x-markdown-footer"
>
{footer}
</div>
);
}, [footer, prefixCls, footerClassName, mergedFooterStyle]);

if (!displayContent && !footer) return null;

return (
<div className={mergedCls} style={mergedStyle}>
{renderer.render(htmlString)}
<div className={classnames(`${prefixCls}-container`, mergedCls)} style={mergedStyle}>
<div className={`${prefixCls}-content`}>
{displayContent ? renderer.render(htmlString) : null}
</div>
{footerElement}
</div>
);
});
Expand Down
15 changes: 15 additions & 0 deletions packages/x-markdown/src/XMarkdown/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,21 @@ interface XMarkdownProps {
* @description DOMPurify configuration for HTML sanitization and XSS protection
*/
dompurifyConfig?: DOMPurifyConfig;
/**
* @description 显示在文档末尾的 React 节点
* @description React node displayed at the end of document
*/
footer?: React.ReactNode;
/**
* @description 文档末尾的 React 节点的 CSS 类名
* @description CSS class name for the footer React node
*/
footerClassName?: string;
/**
* @description 文档末尾的 React 节点的内联样式
* @description Inline styles for the footer React node
*/
footerStyle?: React.CSSProperties;
}

export type { XMarkdownProps, Token, Tokens, StreamStatus, ComponentProps, StreamingOption };
Loading