chore: auto merge branches (#38093)

chore: sync master into feature
This commit is contained in:
github-actions[bot] 2022-10-18 14:32:45 +00:00 committed by GitHub
commit f8629ac588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 264 additions and 420 deletions

39
.circleci/config.yml Normal file
View File

@ -0,0 +1,39 @@
# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/2.0/configuration-reference
version: 2.1
# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
jobs:
test-argos-ci:
docker:
- image: circleci/node:16-browsers
steps:
- checkout
- run:
name: Install node_modules
command: npm i
- run:
name: Install argos cli
command: npm i fast-glob lodash @argos-ci/core
- run:
name: Install puppeteer
command: node node_modules/puppeteer/install.js
- run:
name: Build dist file
command: npm run dist
- run:
name: Run image screenshot tests
command: npm run test-image
- run:
name: Upload screenshots to Argos CI
command: npm run argos
# The resource_class feature allows configuring CPU and RAM resources for each job. Different resource classes are available for different executors. https://circleci.com/docs/2.0/configuration-reference/#resourceclass
resource_class: large
# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows:
test-argos-ci-workflow:
jobs:
- test-argos-ci

View File

@ -1,63 +0,0 @@
# Upload 📷 UI snapshots to argos server, help visual regression testing.
name: 📷 UI Upload
on:
workflow_run:
workflows: ["📷 UI"]
types:
- completed
permissions:
contents: read
jobs:
upload-ui:
permissions:
actions: read # for dawidd6/action-download-artifact to query and download artifacts
pull-requests: read # for dawidd6/action-download-artifact to query commit hash
name: deploy preview
runs-on: ubuntu-latest
if: >
github.repository == 'ant-design/ant-design' &&
github.event.workflow_run.conclusion == 'success'
steps:
- name: checkout
uses: actions/checkout@v3
- name: Download commit artifact
uses: dawidd6/action-download-artifact@v2.23.0
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
name: commit
- name: Save commit id
id: commit
run: echo "::set-output name=id::$(<commit.txt)"
- name: Download branch artifact
uses: dawidd6/action-download-artifact@v2.23.0
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
name: branch
- name: Save branch id
id: branch
run: echo "::set-output name=id::$(<branch.txt)"
- name: Download snapshots artifact
uses: dawidd6/action-download-artifact@v2.23.0
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
workflow_conclusion: success
name: snapshots
path: imageSnapshots
- name: Install
run: npm i fast-glob lodash @argos-ci/core
- name: Upload on Argos
id: deploy
run: npm run argos
env:
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_PARALLEL_NONCE: ${{ github.run_id }}

View File

@ -1,93 +0,0 @@
name: 📷 UI
on:
pull_request:
types: [opened, synchronize, reopened]
push:
# Cancel prev CI if new commit come
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
imagesnapshot:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: cache package-lock.json
uses: actions/cache@v3
with:
path: package-temp-dir
key: lock-${{ github.sha }}
- uses: actions/setup-node@v3
with:
node-version: '16'
- name: create package-lock.json
run: npm i --package-lock-only --ignore-scripts
- name: hack for single file
run: |
if [ ! -d "package-temp-dir" ]; then
mkdir package-temp-dir
fi
cp package-lock.json package-temp-dir
- name: cache node_modules
id: node_modules_cache_id
uses: actions/cache@v3
with:
path: node_modules
key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }}
- name: install
if: steps.node_modules_cache_id.outputs.cache-hit != 'true'
run: npm ci
- name: test
run: npm run test-image
- name: upload snapshots artifact
uses: actions/upload-artifact@v3
with:
name: snapshots
path: imageSnapshots/
retention-days: 3
- name: Save commit
if: github.event_name == 'pull_request' && github.base_ref == 'master'
run: echo ${{ github.event.pull_request.head.sha }} > ./commit.txt
- name: Save commit
if: github.event_name == 'push'
run: echo ${{ github.sha }} > ./commit.txt
- name: Upload commit
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: commit
path: ./commit.txt
- name: Save branch
if: github.event_name == 'pull_request' && github.base_ref == 'master'
run: echo pull/${{ github.event.pull_request.number }}/merge > ./branch.txt
- name: Save branch
if: github.event_name == 'push'
run: echo ${GITHUB_REF##*/} > ./branch.txt
- name: Upload branch
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: branch
path: ./branch.txt

View File

@ -1,6 +0,0 @@
FROM buildkite/puppeteer:10.0.0
RUN mkdir /app
WORKDIR /app
COPY package.json ./
ENV PATH="${PATH}:/app/node_modules/.bin"
COPY . .

View File

@ -35,10 +35,10 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
const sharpMatcherRegx = /#([\S ]+)$/;
type Section = {
interface Section {
link: string;
top: number;
};
}
export interface AnchorProps {
prefixCls?: string;
@ -89,11 +89,6 @@ export interface AntAnchor {
}
class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigConsumerProps> {
static defaultProps = {
affix: true,
showInkInFixed: false,
};
static contextType = ConfigContext;
state = {
@ -111,20 +106,20 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
private links: string[] = [];
private scrollEvent: any;
private scrollEvent: ReturnType<typeof addEventListener>;
private animating: boolean;
private prefixCls?: string;
// Context
registerLink = (link: string) => {
registerLink: AntAnchor['registerLink'] = link => {
if (!this.links.includes(link)) {
this.links.push(link);
}
};
unregisterLink = (link: string) => {
unregisterLink: AntAnchor['unregisterLink'] = link => {
const index = this.links.indexOf(link);
if (index !== -1) {
this.links.splice(index, 1);
@ -174,7 +169,7 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
const linkSections: Array<Section> = [];
const container = this.getContainer();
this.links.forEach(link => {
const sharpLinkMatch = sharpMatcherRegx.exec(link.toString());
const sharpLinkMatch = sharpMatcherRegx.exec(link?.toString());
if (!sharpLinkMatch) {
return;
}
@ -182,10 +177,7 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
if (target) {
const top = getOffsetTop(target, container);
if (top < offsetTop + bounds) {
linkSections.push({
link,
top,
});
linkSections.push({ link, top });
}
}
});
@ -259,10 +251,9 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
updateInk = () => {
const { prefixCls, wrapperRef } = this;
const anchorNode = wrapperRef.current;
const linkNode = anchorNode?.getElementsByClassName(`${prefixCls}-link-title-active`)[0];
const linkNode = anchorNode?.querySelector<HTMLElement>(`.${prefixCls}-link-title-active`);
if (linkNode) {
this.inkNode.style.top = `${(linkNode as any).offsetTop + linkNode.clientHeight / 2 - 4.5}px`;
this.inkNode.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2 - 4.5}px`;
}
};
@ -283,8 +274,8 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
className = '',
style,
offsetTop,
affix,
showInkInFixed,
affix = true,
showInkInFixed = false,
children,
onClick,
} = this.props;
@ -311,7 +302,7 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
[`${prefixCls}-fixed`]: !affix && !showInkInFixed,
});
const wrapperStyle = {
const wrapperStyle: React.CSSProperties = {
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
...style,
};
@ -331,33 +322,26 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
return (
<AnchorContext.Provider value={contextValue}>
{!affix ? (
anchorContent
) : (
{affix ? (
<Affix offsetTop={offsetTop} target={this.getContainer}>
{anchorContent}
</Affix>
) : (
anchorContent
)}
</AnchorContext.Provider>
);
}
}
// just use in test
export type InternalAnchorClass = Anchor;
const AnchorFC = React.forwardRef<Anchor, AnchorProps>((props, ref) => {
const { prefixCls: customizePrefixCls } = props;
const { getPrefixCls } = React.useContext(ConfigContext);
const anchorPrefixCls = getPrefixCls('anchor', customizePrefixCls);
const anchorProps: InternalAnchorProps = {
...props,
anchorPrefixCls,
};
return <Anchor {...anchorProps} ref={ref} />;
return <Anchor {...props} ref={ref} anchorPrefixCls={anchorPrefixCls} />;
});
export default AnchorFC;

View File

@ -14,71 +14,53 @@ export interface AnchorLinkProps {
className?: string;
}
class AnchorLink extends React.Component<AnchorLinkProps, any, AntAnchor> {
static defaultProps = {
href: '#',
};
const AnchorLink: React.FC<AnchorLinkProps> = props => {
const { href = '#', title, prefixCls: customizePrefixCls, children, className, target } = props;
static contextType = AnchorContext;
const context = React.useContext<AntAnchor | undefined>(AnchorContext);
context: AntAnchor;
const { registerLink, unregisterLink, scrollTo, onClick, activeLink } = context || {};
componentDidMount() {
this.context.registerLink(this.props.href);
}
React.useEffect(() => {
registerLink?.(href);
return () => {
unregisterLink?.(href);
};
}, [href]);
componentDidUpdate({ href: prevHref }: AnchorLinkProps) {
const { href } = this.props;
if (prevHref !== href) {
this.context.unregisterLink(prevHref);
this.context.registerLink(href);
}
}
componentWillUnmount() {
this.context.unregisterLink(this.props.href);
}
handleClick = (e: React.MouseEvent<HTMLElement>) => {
const { scrollTo, onClick } = this.context;
const { href, title } = this.props;
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
onClick?.(e, { title, href });
scrollTo(href);
scrollTo?.(href);
};
renderAnchorLink = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, href, title, children, className, target } = this.props;
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
const active = this.context.activeLink === href;
const wrapperClassName = classNames(
`${prefixCls}-link`,
{
[`${prefixCls}-link-active`]: active,
},
className,
);
const titleClassName = classNames(`${prefixCls}-link-title`, {
[`${prefixCls}-link-title-active`]: active,
});
return (
<div className={wrapperClassName}>
<a
className={titleClassName}
href={href}
title={typeof title === 'string' ? title : ''}
target={target}
onClick={this.handleClick}
>
{title}
</a>
{children}
</div>
);
};
render() {
return <ConfigConsumer>{this.renderAnchorLink}</ConfigConsumer>;
}
}
return (
<ConfigConsumer>
{({ getPrefixCls }: ConfigConsumerProps) => {
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
const active = activeLink === href;
const wrapperClassName = classNames(`${prefixCls}-link`, className, {
[`${prefixCls}-link-active`]: active,
});
const titleClassName = classNames(`${prefixCls}-link-title`, {
[`${prefixCls}-link-title-active`]: active,
});
return (
<div className={wrapperClassName}>
<a
className={titleClassName}
href={href}
title={typeof title === 'string' ? title : ''}
target={target}
onClick={handleClick}
>
{title}
</a>
{children}
</div>
);
}}
</ConfigConsumer>
);
};
export default AnchorLink;

View File

@ -41,11 +41,17 @@ describe('Anchor Render', () => {
getClientRectsMock.mockReturnValue({ length: 1 } as DOMRectList);
});
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});
afterAll(() => {
jest.clearAllTimers();
jest.useRealTimers();
getBoundingClientRectMock.mockRestore();
getClientRectsMock.mockRestore();
@ -69,7 +75,7 @@ describe('Anchor Render', () => {
expect(anchorInstance!.state).not.toBe(null);
});
it('Anchor render perfectly for complete href - click', () => {
it('Anchor render perfectly for complete href - click', async () => {
const hash = getHashUrl();
let anchorInstance: InternalAnchorClass;
const { container } = render(
@ -82,6 +88,7 @@ describe('Anchor Render', () => {
</Anchor>,
);
fireEvent.click(container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!);
await waitFakeTimer();
expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`);
});
@ -100,13 +107,12 @@ describe('Anchor Render', () => {
</Anchor>,
);
anchorInstance!.handleScrollTo('/#/faq?locale=en#Q1');
expect(anchorInstance!.state.activeLink).toBe('/#/faq?locale=en#Q1');
expect(scrollToSpy).not.toHaveBeenCalled();
await waitFakeTimer();
expect(anchorInstance!.state.activeLink).toBe('/#/faq?locale=en#Q1');
expect(scrollToSpy).toHaveBeenCalled();
});
it('Anchor render perfectly for complete href - scroll', () => {
it('Anchor render perfectly for complete href - scroll', async () => {
const hash = getHashUrl();
const root = createDiv();
render(<div id={hash}>Hello</div>, { container: root });
@ -121,6 +127,7 @@ describe('Anchor Render', () => {
</Anchor>,
);
anchorInstance!.handleScroll();
await waitFakeTimer();
expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`);
});
@ -141,10 +148,10 @@ describe('Anchor Render', () => {
);
anchorInstance!.handleScrollTo(`##${hash}`);
await waitFakeTimer();
expect(anchorInstance!.state.activeLink).toBe(`##${hash}`);
const calls = scrollToSpy.mock.calls.length;
await waitFakeTimer();
expect(scrollToSpy.mock.calls.length).toBeGreaterThan(calls);
expect(scrollToSpy.mock.calls.length).toBe(calls);
});
it('should remove listener when unmount', async () => {
@ -373,7 +380,7 @@ describe('Anchor Render', () => {
it('Anchor targetOffset prop', async () => {
const hash = getHashUrl();
let dateNowMock;
let dateNowMock: jest.SpyInstance;
function dataNowMockFn() {
let start = 0;
@ -438,7 +445,7 @@ describe('Anchor Render', () => {
// https://github.com/ant-design/ant-design/issues/31941
it('Anchor targetOffset prop when contain spaces', async () => {
const hash = `${getHashUrl()} s p a c e s`;
let dateNowMock;
let dateNowMock: jest.SpyInstance;
function dataNowMockFn() {
let start = 0;
@ -543,13 +550,9 @@ describe('Anchor Render', () => {
});
it('test edge case when getBoundingClientRect return zero size', async () => {
getBoundingClientRectMock.mockReturnValue({
width: 0,
height: 0,
top: 1000,
} as DOMRect);
getBoundingClientRectMock.mockReturnValue({ width: 0, height: 0, top: 1000 } as DOMRect);
const hash = getHashUrl();
let dateNowMock;
let dateNowMock: jest.SpyInstance;
function dataNowMockFn() {
let start = 0;
@ -615,7 +618,7 @@ describe('Anchor Render', () => {
it('test edge case when container is not windows', async () => {
const hash = getHashUrl();
let dateNowMock;
let dateNowMock: jest.SpyInstance;
function dataNowMockFn() {
let start = 0;
@ -758,5 +761,15 @@ describe('Anchor Render', () => {
rerender(<Demo current={hash2} />);
expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2);
});
it('should correct render when href is null', () => {
expect(() => {
render(
<Anchor>
<Link href={null as unknown as string} title="test" />
</Anchor>,
);
fireEvent.scroll(window || document);
}).not.toThrow();
});
});
});

View File

@ -1,6 +1,6 @@
import * as React from 'react';
import type { AntAnchor } from './Anchor';
const AnchorContext = React.createContext<AntAnchor>(null as any);
const AnchorContext = React.createContext<AntAnchor | undefined>(undefined);
export default AnchorContext;

View File

@ -619,37 +619,48 @@ describe('Form', () => {
jest.useFakeTimers();
const shouldNotRender = jest.fn();
const StaticInput: React.FC = () => {
const StaticInput: React.FC<React.InputHTMLAttributes<HTMLInputElement>> = ({
id,
value = '',
}) => {
shouldNotRender();
return <Input />;
return <input id={id} value={value} />;
};
const shouldRender = jest.fn();
const DynamicInput: React.FC = () => {
const DynamicInput: React.FC<React.InputHTMLAttributes<HTMLInputElement>> = ({
value = '',
id,
}) => {
shouldRender();
return <Input />;
return <input id={id} value={value} />;
};
const formRef = React.createRef<FormInstance>();
pureRender(
const { container } = pureRender(
<Form ref={formRef}>
<Form.Item>
<StaticInput />
</Form.Item>
<Form.Item name="light">
<DynamicInput />
<DynamicInput id="changed" />
</Form.Item>
</Form>,
);
await waitFakeTimer();
expect(container.querySelector<HTMLInputElement>('#changed')!.value).toEqual('');
expect(shouldNotRender).toHaveBeenCalledTimes(1);
expect(shouldRender).toHaveBeenCalledTimes(1);
formRef.current?.setFieldsValue({ light: 'bamboo' });
formRef.current!.setFieldsValue({ light: 'bamboo' });
await waitFakeTimer(100, 100);
await waitFakeTimer();
expect(formRef.current!.getFieldsValue()).toEqual({ light: 'bamboo' });
expect(container.querySelector<HTMLInputElement>('#changed')!.value).toEqual('bamboo');
expect(shouldNotRender).toHaveBeenCalledTimes(1);
expect(shouldRender).toHaveBeenCalledTimes(2);

View File

@ -18,7 +18,8 @@ const localeValues: Locale = {
filterConfirm: 'OK',
filterReset: 'Réinitialiser',
filterEmptyText: 'Aucun filtre',
filterCheckall: 'Sélectionner la page actuelle',
filterCheckall: 'Tout sélectionner',
filterSearchPlaceholder: 'Chercher dans les filtres',
emptyText: 'Aucune donnée',
selectAll: 'Sélectionner la page actuelle',
selectInvert: 'Inverser la sélection de la page actuelle',

View File

@ -146,7 +146,7 @@ One of the Table `columns` prop for describing the table's columns, Column has t
| showSorterTooltip | If header show next sorter direction tooltip, override `showSorterTooltip` in table | boolean \| [Tooltip props](/components/tooltip/) | true | |
| sortDirections | Supported sort way, override `sortDirections` in `Table`, could be `ascend`, `descend` | Array | \[`ascend`, `descend`] | |
| sorter | Sort function for local sort, see [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)'s compareFunction. If you need sort buttons only, set to `true` | function \| boolean | - | |
| sortOrder | Order of sorted values: `'ascend'` `'descend'` `false` | boolean \| string | - | |
| sortOrder | Order of sorted values: `ascend` `descend` `null` | `ascend` \| `descend` \| null | - | |
| title | Title of this column | ReactNode \| ({ sortOrder, sortColumn, filters }) => ReactNode | - | |
| width | Width of this column ([width not working?](https://github.com/ant-design/ant-design/issues/13825#issuecomment-449889241)) | string \| number | - | |
| onCell | Set props on per cell | function(record, rowIndex) | - | |

View File

@ -147,7 +147,7 @@ const columns = [
| showSorterTooltip | 表头显示下一次排序的 tooltip 提示, 覆盖 table 中 `showSorterTooltip` | boolean \| [Tooltip props](/components/tooltip/#API) | true | |
| sortDirections | 支持的排序方式,覆盖 `Table``sortDirections` 取值为 `ascend` `descend` | Array | \[`ascend`, `descend`] | |
| sorter | 排序函数,本地排序使用一个函数(参考 [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) 的 compareFunction),需要服务端排序可设为 true | function \| boolean | - | |
| sortOrder | 排序的受控属性,外界可用此控制列的排序,可设置为 `ascend` `descend` false | boolean \| string | - | |
| sortOrder | 排序的受控属性,外界可用此控制列的排序,可设置为 `ascend` `descend` `null` | `ascend` \| `descend` \| null | - | |
| title | 列头显示文字(函数用法 `3.10.0` 后支持) | ReactNode \| ({ sortOrder, sortColumn, filters }) => ReactNode | - | |
| width | 列宽度([指定了也不生效?](https://github.com/ant-design/ant-design/issues/13825#issuecomment-449889241) | string \| number | - | |
| onCell | 设置单元格属性 | function(record, rowIndex) | - | |

View File

@ -111,7 +111,7 @@ File icon realize by using switcherIcon. You can overwrite the style to hide it:
### Why defaultExpandAll not working on ajax data?
`default` prefix prop only works when inited. So `defaultExpandAll` has already executed when ajax load data. You can control `expandedKeys` or render Tree when data loaded to realize expanded all.
`default` prefix prop only works when initializing. So `defaultExpandAll` has already executed when ajax load data. You can control `expandedKeys` or render Tree when data loaded to realize expanded all.
### Virtual scroll limitation

View File

@ -4,7 +4,9 @@
@select-tree-prefix-cls: ~'@{ant-prefix}-select-tree';
@tree-motion: ~'@{ant-prefix}-motion-collapse';
@tree-node-padding: (@padding-xs / 2);
// @deprecated: kept for customization usages, recommend using @tree-node-highlight-color instead.
@tree-node-hightlight-color: inherit;
@tree-node-highlight-color: @tree-node-hightlight-color;
.antTreeSwitcherIcon(@type: 'tree-default-open-icon') {
.@{tree-prefix-cls}-switcher-icon,
@ -114,7 +116,7 @@
}
&:not(&-disabled).filter-node .@{custom-tree-prefix-cls}-title {
color: @tree-node-hightlight-color;
color: @tree-node-highlight-color;
font-weight: 500;
}

View File

@ -67,6 +67,13 @@ const WALKING = 2;
const DONE_WITH_ELLIPSIS = 3;
const DONE_WITHOUT_ELLIPSIS = 4;
type WalkingState =
| typeof NONE
| typeof PREPARE
| typeof WALKING
| typeof DONE_WITH_ELLIPSIS
| typeof DONE_WITHOUT_ELLIPSIS;
const Ellipsis = ({
enabledMeasure,
children,
@ -76,20 +83,15 @@ const Ellipsis = ({
rows,
onEllipsis,
}: EllipsisProps) => {
const [cutLength, setCutLength] = React.useState<[number, number, number]>([0, 0, 0]);
const [walkingState, setWalkingState] = React.useState<
| typeof NONE
| typeof PREPARE
| typeof WALKING
| typeof DONE_WITH_ELLIPSIS
| typeof DONE_WITHOUT_ELLIPSIS
>(NONE);
const [startLen, midLen, endLen] = cutLength;
const [[startLen, midLen, endLen], setCutLength] = React.useState<
[startLen: number, midLen: number, endLen: number]
>([0, 0, 0]);
const [walkingState, setWalkingState] = React.useState<WalkingState>(NONE);
const [singleRowHeight, setSingleRowHeight] = React.useState(0);
const singleRowRef = React.useRef<HTMLSpanElement>(null);
const midRowRef = React.useRef<HTMLSpanElement>(null);
const singleRowRef = React.useRef<HTMLElement>(null);
const midRowRef = React.useRef<HTMLElement>(null);
const nodeList = React.useMemo(() => toArray(text), [text]);
const totalLen = React.useMemo(() => getNodesLen(nodeList), [nodeList]);
@ -167,7 +169,7 @@ const Ellipsis = ({
const renderMeasure = (
content: React.ReactNode,
ref: React.Ref<HTMLSpanElement>,
ref: React.Ref<HTMLElement>,
style: React.CSSProperties,
) => (
<span
@ -189,7 +191,7 @@ const Ellipsis = ({
</span>
);
const renderMeasureSlice = (len: number, ref: React.Ref<HTMLSpanElement>) => {
const renderMeasureSlice = (len: number, ref: React.Ref<HTMLElement>) => {
const sliceNodeList = sliceNodes(nodeList, len);
return renderMeasure(children(sliceNodeList, true), ref, measureStyle);

View File

@ -60,7 +60,8 @@ export interface EllipsisConfig {
tooltip?: React.ReactNode | TooltipProps;
}
export interface BlockProps extends TypographyProps {
export interface BlockProps<C extends keyof JSX.IntrinsicElements = keyof JSX.IntrinsicElements>
extends TypographyProps<C> {
title?: string;
editable?: boolean | EditConfig;
copyable?: boolean | CopyConfig;
@ -114,13 +115,9 @@ function toList<T extends any>(val: T | T[]): T[] {
return Array.isArray(val) ? val : [val];
}
interface InternalBlockProps extends BlockProps {
component: string;
}
const ELLIPSIS_STR = '...';
const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
const {
prefixCls: customizePrefixCls,
className,
@ -152,7 +149,7 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
'strong',
'keyboard',
'italic',
]) as any;
]);
// ========================== Editable ==========================
const [enableEdit, editConfig] = useMergedConfig<EditConfig>(editable);
@ -176,7 +173,7 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
}
}, [editing]);
const onEditClick = (e?: React.MouseEvent<HTMLDivElement>) => {
const onEditClick = (e?: React.MouseEvent<HTMLElement>) => {
e?.preventDefault();
triggerEdit(true);
};
@ -194,7 +191,7 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
// ========================== Copyable ==========================
const [enableCopy, copyConfig] = useMergedConfig<CopyConfig>(copyable);
const [copied, setCopied] = React.useState(false);
const copyIdRef = React.useRef<NodeJS.Timeout>();
const copyIdRef = React.useRef<number>();
const copyOptions: Pick<CopyConfig, 'format'> = {};
if (copyConfig.format) {
@ -202,7 +199,7 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
}
const cleanCopyId = () => {
clearTimeout(copyIdRef.current!);
window.clearTimeout(copyIdRef.current!);
};
const onCopyClick = (e?: React.MouseEvent<HTMLDivElement>) => {
@ -215,7 +212,7 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
// Trigger tips update
cleanCopyId();
copyIdRef.current = setTimeout(() => {
copyIdRef.current = window.setTimeout(() => {
setCopied(false);
}, 3000);
@ -352,7 +349,7 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
tooltipProps = { title: ellipsisConfig.tooltip };
}
const topAriaLabel = React.useMemo(() => {
const isValid = (val: any) => ['string', 'number'].includes(typeof val);
const isValid = (val: any): val is string | number => ['string', 'number'].includes(typeof val);
if (!enableEllipsis || cssEllipsis) {
return undefined;
@ -495,7 +492,7 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
return (
<ResizeObserver onResize={onResize} disabled={!mergedEnableEllipsis || cssEllipsis}>
{resizeRef => (
{(resizeRef: React.RefObject<HTMLElement>) => (
<EllipsisTooltip
tooltipProps={tooltipProps}
enabledEllipsis={mergedEnableEllipsis}
@ -520,8 +517,8 @@ const Base = React.forwardRef((props: InternalBlockProps, ref: any) => {
component={component}
ref={composeRef(resizeRef, typographyRef, ref)}
direction={direction}
onClick={triggerType.includes('text') ? onEditClick : null}
aria-label={topAriaLabel}
onClick={triggerType.includes('text') ? onEditClick : undefined}
aria-label={topAriaLabel?.toString()}
title={title}
{...textProps}
>

View File

@ -4,15 +4,12 @@ import type { BlockProps } from './Base';
import Base from './Base';
export interface LinkProps
extends BlockProps,
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'type'> {
extends BlockProps<'a'>,
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'type' | keyof BlockProps<'a'>> {
ellipsis?: boolean;
}
const Link: React.ForwardRefRenderFunction<HTMLElement, LinkProps> = (
{ ellipsis, rel, ...restProps },
ref,
) => {
const Link = React.forwardRef<HTMLElement, LinkProps>(({ ellipsis, rel, ...restProps }, ref) => {
warning(
typeof ellipsis !== 'object',
'Typography.Link',
@ -28,11 +25,10 @@ const Link: React.ForwardRefRenderFunction<HTMLElement, LinkProps> = (
rel: rel === undefined && restProps.target === '_blank' ? 'noopener noreferrer' : rel,
};
// https://github.com/ant-design/ant-design/issues/26622
// @ts-ignore
// @ts-expect-error: https://github.com/ant-design/ant-design/issues/26622
delete mergedProps.navigate;
return <Base {...mergedProps} ref={baseRef} ellipsis={!!ellipsis} component="a" />;
};
});
export default React.forwardRef(Link);
export default Link;

View File

@ -2,12 +2,12 @@ import * as React from 'react';
import type { BlockProps } from './Base';
import Base from './Base';
export interface ParagraphProps extends BlockProps {
onClick?: (e?: React.MouseEvent<HTMLDivElement>) => void;
}
export interface ParagraphProps
extends BlockProps<'div'>,
Omit<React.HTMLAttributes<HTMLDivElement>, 'type' | keyof BlockProps<'div'>> {}
const Paragraph: React.ForwardRefRenderFunction<HTMLDivElement, ParagraphProps> = (props, ref) => (
const Paragraph = React.forwardRef<HTMLElement, ParagraphProps>((props, ref) => (
<Base ref={ref} {...props} component="div" />
);
));
export default React.forwardRef(Paragraph);
export default Paragraph;

View File

@ -4,9 +4,10 @@ import warning from '../_util/warning';
import type { BlockProps, EllipsisConfig } from './Base';
import Base from './Base';
export interface TextProps extends BlockProps {
export interface TextProps
extends BlockProps<'span'>,
Omit<React.HTMLAttributes<HTMLSpanElement>, 'type' | keyof BlockProps<'span'>> {
ellipsis?: boolean | Omit<EllipsisConfig, 'expandable' | 'rows' | 'onExpand'>;
onClick?: (e?: React.MouseEvent<HTMLDivElement>) => void;
}
const Text: React.ForwardRefRenderFunction<HTMLSpanElement, TextProps> = (

View File

@ -6,17 +6,18 @@ import Base from './Base';
const TITLE_ELE_LIST = tupleNum(1, 2, 3, 4, 5);
export type TitleProps = Omit<
BlockProps & {
level?: typeof TITLE_ELE_LIST[number];
onClick?: (e?: React.MouseEvent<HTMLDivElement>) => void;
},
'strong'
>;
export interface TitleProps
extends Omit<BlockProps<'h1' | 'h2' | 'h3' | 'h4' | 'h5'>, 'strong'>,
Omit<
React.HTMLAttributes<HTMLHeadElement>,
'type' | keyof BlockProps<'h1' | 'h2' | 'h3' | 'h4' | 'h5'>
> {
level?: typeof TITLE_ELE_LIST[number];
}
const Title: React.ForwardRefRenderFunction<HTMLHeadingElement, TitleProps> = (props, ref) => {
const Title = React.forwardRef<HTMLElement, TitleProps>((props, ref) => {
const { level = 1, ...restProps } = props;
let component: string;
let component: keyof JSX.IntrinsicElements;
if (TITLE_ELE_LIST.indexOf(level) !== -1) {
component = `h${level}`;
@ -30,6 +31,6 @@ const Title: React.ForwardRefRenderFunction<HTMLHeadingElement, TitleProps> = (p
}
return <Base ref={ref} {...restProps} component={component} />;
};
});
export default React.forwardRef(Title);
export default Title;

View File

@ -1,66 +1,76 @@
import classNames from 'classnames';
import { composeRef } from 'rc-util/lib/ref';
import * as React from 'react';
import type { DirectionType } from '../config-provider';
import { ConfigContext } from '../config-provider';
import warning from '../_util/warning';
export interface TypographyProps {
export interface TypographyProps<C extends keyof JSX.IntrinsicElements>
extends React.HTMLAttributes<HTMLElement> {
id?: string;
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
/** @internal */
component?: C;
['aria-label']?: string;
direction?: DirectionType;
}
interface InternalTypographyProps extends TypographyProps {
component?: string;
interface InternalTypographyProps<C extends keyof JSX.IntrinsicElements>
extends TypographyProps<C> {
/** @deprecated Use `ref` directly if using React 16 */
setContentRef?: (node: HTMLElement) => void;
}
const Typography: React.ForwardRefRenderFunction<{}, InternalTypographyProps> = (
{
prefixCls: customizePrefixCls,
component = 'article',
className,
'aria-label': ariaLabel,
setContentRef,
children,
...restProps
},
ref,
) => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
let mergedRef = ref;
if (setContentRef) {
warning(false, 'Typography', '`setContentRef` is deprecated. Please use `ref` instead.');
mergedRef = composeRef(ref, setContentRef);
}
const Component = component as any;
const prefixCls = getPrefixCls('typography', customizePrefixCls);
const componentClassName = classNames(
prefixCls,
const Typography = React.forwardRef<
HTMLElement,
InternalTypographyProps<keyof JSX.IntrinsicElements>
>(
(
{
[`${prefixCls}-rtl`]: direction === 'rtl',
prefixCls: customizePrefixCls,
component: Component = 'article',
className,
setContentRef,
children,
direction: typographyDirection,
...restProps
},
className,
);
return (
<Component className={componentClassName} aria-label={ariaLabel} ref={mergedRef} {...restProps}>
{children}
</Component>
);
};
ref,
) => {
const { getPrefixCls, direction: contextDirection } = React.useContext(ConfigContext);
const direction = typographyDirection ?? contextDirection;
let mergedRef = ref;
if (setContentRef) {
warning(false, 'Typography', '`setContentRef` is deprecated. Please use `ref` instead.');
mergedRef = composeRef(ref, setContentRef);
}
const prefixCls = getPrefixCls('typography', customizePrefixCls);
const componentClassName = classNames(
prefixCls,
{
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);
return (
// @ts-expect-error: Expression produces a union type that is too complex to represent.
<Component className={componentClassName} ref={mergedRef} {...restProps}>
{children}
</Component>
);
},
);
const RefTypography = React.forwardRef(Typography);
if (process.env.NODE_ENV !== 'production') {
RefTypography.displayName = 'Typography';
Typography.displayName = 'Typography';
}
// es default export should use const instead of let
const ExportTypography = RefTypography as unknown as React.FC<TypographyProps>;
export default ExportTypography;
export default Typography;

View File

@ -3,27 +3,23 @@
exports[`Typography rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-typography ant-typography-rtl"
direction="rtl"
/>
`;
exports[`Typography rtl render component should be rendered correctly in RTL direction 2`] = `
<article
class="ant-typography ant-typography-rtl"
direction="rtl"
/>
`;
exports[`Typography rtl render component should be rendered correctly in RTL direction 3`] = `
<h1
class="ant-typography ant-typography-rtl"
direction="rtl"
/>
`;
exports[`Typography rtl render component should be rendered correctly in RTL direction 4`] = `
<a
class="ant-typography ant-typography-rtl"
direction="rtl"
/>
`;

View File

@ -382,21 +382,14 @@ describe('Typography.Ellipsis', () => {
it('js ellipsis should show aria-label', () => {
const { container: titleWrapper } = render(
<Base
component={undefined as unknown as string}
title="bamboo"
ellipsis={{ expandable: true }}
/>,
<Base component={undefined} title="bamboo" ellipsis={{ expandable: true }} />,
);
expect(titleWrapper.querySelector('.ant-typography')?.getAttribute('aria-label')).toEqual(
'bamboo',
);
const { container: tooltipWrapper } = render(
<Base
component={undefined as unknown as string}
ellipsis={{ expandable: true, tooltip: 'little' }}
/>,
<Base component={undefined} ellipsis={{ expandable: true, tooltip: 'little' }} />,
);
expect(tooltipWrapper.querySelector('.ant-typography')?.getAttribute('aria-label')).toEqual(
'little',
@ -426,11 +419,7 @@ describe('Typography.Ellipsis', () => {
const ref = React.createRef<any>();
const { container, baseElement } = render(
<Base
component={undefined as unknown as string}
ellipsis={{ tooltip: 'This is tooltip', rows: 2 }}
ref={ref}
>
<Base component={undefined} ellipsis={{ tooltip: 'This is tooltip', rows: 2 }} ref={ref}>
Ant Design, a design language for background applications, is refined by Ant UED Team.
</Base>,
);

View File

@ -1,17 +0,0 @@
version: '3'
services:
tests:
build:
context: .
dockerfile: Dockerfile.ui-test
volumes:
- './node_modules:/app/node_modules'
- './components:/app/components'
- './tests:/app/tests'
- './jest-stare:/app/jest-stare'
- './dist:/app/dist'
- '.jest.image.js:/app/.jest.image.js'
- './jest-puppeteer.config.js:/app/jest-puppeteer.config.js'
- './imageSnapshots:/app/imageSnapshots'
- './imageDiffSnapshots:/app/imageDiffSnapshots'
entrypoint: "npm run test-image:docker"

View File

@ -98,8 +98,7 @@
"test-node": "jest --config .jest.node.js --cache=false",
"tsc": "tsc --noEmit",
"site:test": "jest --config .jest.site.js --cache=false --force-exit",
"test-image": "npm run dist && docker-compose run tests",
"test-image:docker": "node node_modules/puppeteer/install.js && jest --config .jest.image.js --no-cache -i",
"test-image": "npm run dist && jest --config .jest.image.js --no-cache -i -u",
"argos": "node ./scripts/argos-upload.js",
"version": "node ./scripts/generate-version",
"install-react-16": "npm i --no-save --legacy-peer-deps react@16 react-dom@16",
@ -191,7 +190,7 @@
"@types/warning": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"antd-img-crop": "4.2.5",
"antd-img-crop": "^4.2.8",
"array-move": "^4.0.0",
"babel-plugin-add-react-displayname": "^0.0.5",
"bisheng": "^3.7.0-alpha.4",

View File

@ -47,7 +47,7 @@ async function run() {
token: process.env.ARGOS_TOKEN,
parallel: {
total: chunks.length,
nonce: process.env.ARGOS_PARALLEL_NONCE,
nonce: process.env.ARGOS_PARALLEL_NONCE || process.env.CIRCLE_BUILD_NUM,
},
});
// eslint-disable-next-line no-console -- pipe stdout