Browse Source

Merge pull request #40771 from ant-design/master

chore: feature merge master
pull/40801/head
MadCcc 2 years ago
committed by GitHub
parent
commit
1ebdfb7049
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      components/cascader/index.en-US.md
  2. 1
      components/cascader/index.zh-CN.md
  3. 13
      components/date-picker/style/index.ts
  4. 7
      components/locale/fr_FR.ts
  5. 21
      components/radio/__tests__/radio.test.tsx
  6. 6
      components/radio/radio.tsx
  7. 30
      components/skeleton/style/index.tsx
  8. 1
      components/tree-select/index.en-US.md
  9. 1
      components/tree-select/index.zh-CN.md
  10. 127
      docs/blog/tooltip-align.en-US.md
  11. 127
      docs/blog/tooltip-align.zh-CN.md
  12. 2
      docs/spec/cases.en-US.md
  13. 2
      docs/spec/cases.zh-CN.md
  14. 8
      package.json

1
components/cascader/index.en-US.md

@ -64,6 +64,7 @@ Cascade selection box.
| loadData | To load option lazily, and it cannot work with `showSearch` | (selectedOptions) => void | - | | | loadData | To load option lazily, and it cannot work with `showSearch` | (selectedOptions) => void | - | |
| maxTagCount | Max tag count to show. `responsive` will cost render performance | number \| `responsive` | - | 4.17.0 | | maxTagCount | Max tag count to show. `responsive` will cost render performance | number \| `responsive` | - | 4.17.0 |
| maxTagPlaceholder | Placeholder for not showing tags | ReactNode \| function(omittedValues) | - | 4.17.0 | | maxTagPlaceholder | Placeholder for not showing tags | ReactNode \| function(omittedValues) | - | 4.17.0 |
| maxTagTextLength | Max tag text length to show | number | - | 4.17.0 |
| notFoundContent | Specify content to show when no result matches | string | `Not Found` | | | notFoundContent | Specify content to show when no result matches | string | `Not Found` | |
| open | Set visible of cascader popup | boolean | - | 4.17.0 | | open | Set visible of cascader popup | boolean | - | 4.17.0 |
| options | The data options of cascade | [Option](#option)\[] | - | | | options | The data options of cascade | [Option](#option)\[] | - | |

1
components/cascader/index.zh-CN.md

@ -65,6 +65,7 @@ demo:
| loadData | 用于动态加载选项,无法与 `showSearch` 一起使用 | (selectedOptions) => void | - | | | loadData | 用于动态加载选项,无法与 `showSearch` 一起使用 | (selectedOptions) => void | - | |
| maxTagCount | 最多显示多少个 tag,响应式模式会对性能产生损耗 | number \| `responsive` | - | 4.17.0 | | maxTagCount | 最多显示多少个 tag,响应式模式会对性能产生损耗 | number \| `responsive` | - | 4.17.0 |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | 4.17.0 | | maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | 4.17.0 |
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | 4.17.0 |
| notFoundContent | 当下拉列表为空时显示的内容 | string | `Not Found` | | | notFoundContent | 当下拉列表为空时显示的内容 | string | `Not Found` | |
| open | 控制浮层显隐 | boolean | - | 4.17.0 | | open | 控制浮层显隐 | boolean | - | 4.17.0 |
| options | 可选项数据源 | [Option](#option)\[] | - | | | options | 可选项数据源 | [Option](#option)\[] | - | |

13
components/date-picker/style/index.ts

@ -37,6 +37,7 @@ export type PickerPanelToken = {
pickerDateHoverRangeBorderColor: string; pickerDateHoverRangeBorderColor: string;
pickerBasicCellHoverWithRangeColor: string; pickerBasicCellHoverWithRangeColor: string;
pickerPanelWithoutTimeCellHeight: number; pickerPanelWithoutTimeCellHeight: number;
pickerDatePanelPaddingHorizontal: number;
pickerYearMonthCellWidth: number; pickerYearMonthCellWidth: number;
pickerTimePanelColumnHeight: number; pickerTimePanelColumnHeight: number;
pickerTimePanelColumnWidth: number; pickerTimePanelColumnWidth: number;
@ -327,9 +328,10 @@ export const genPanelStyle = (token: SharedPickerToken): CSSObject => {
pickerTimePanelCellHeight, pickerTimePanelCellHeight,
controlItemBgActive, controlItemBgActive,
marginXXS, marginXXS,
pickerDatePanelPaddingHorizontal,
} = token; } = token;
const pickerPanelWidth = pickerPanelCellWidth * 7 + paddingSM * 2 + 4; const pickerPanelWidth = pickerPanelCellWidth * 7 + pickerDatePanelPaddingHorizontal * 2;
const commonHoverCellFixedDistance = const commonHoverCellFixedDistance =
(pickerPanelWidth - paddingXS * 2) / 3 - pickerYearMonthCellWidth - paddingSM; (pickerPanelWidth - paddingXS * 2) / 3 - pickerYearMonthCellWidth - paddingSM;
@ -400,6 +402,7 @@ export const genPanelStyle = (token: SharedPickerToken): CSSObject => {
border: 0, border: 0,
cursor: 'pointer', cursor: 'pointer',
transition: `color ${motionDurationMid}`, transition: `color ${motionDurationMid}`,
fontSize: 'inherit',
}, },
'> button': { '> button': {
@ -765,7 +768,7 @@ export const genPanelStyle = (token: SharedPickerToken): CSSObject => {
// ====================== Date Panel ====================== // ====================== Date Panel ======================
'&-date-panel': { '&-date-panel': {
[`${componentCls}-body`]: { [`${componentCls}-body`]: {
padding: `${paddingXS}px ${paddingSM}px`, padding: `${paddingXS}px ${pickerDatePanelPaddingHorizontal}px`,
}, },
[`${componentCls}-content`]: { [`${componentCls}-content`]: {
@ -1435,7 +1438,8 @@ const genPickerStyle: GenerateStyle<PickerToken> = (token) => {
export const initPickerPanelToken = (token: TokenWithCommonCls<GlobalToken>): PickerPanelToken => { export const initPickerPanelToken = (token: TokenWithCommonCls<GlobalToken>): PickerPanelToken => {
const pickerTimePanelCellHeight = 28; const pickerTimePanelCellHeight = 28;
const { componentCls, controlHeightLG, controlHeightSM, colorPrimary, paddingXXS } = token; const { componentCls, controlHeightLG, controlHeightSM, colorPrimary, paddingXXS, padding } =
token;
return { return {
pickerCellCls: `${componentCls}-cell`, pickerCellCls: `${componentCls}-cell`,
@ -1451,10 +1455,11 @@ export const initPickerPanelToken = (token: TokenWithCommonCls<GlobalToken>): Pi
pickerTimePanelColumnWidth: controlHeightLG * 1.4, pickerTimePanelColumnWidth: controlHeightLG * 1.4,
pickerTimePanelCellHeight, pickerTimePanelCellHeight,
pickerQuarterPanelContentHeight: controlHeightLG * 1.4, pickerQuarterPanelContentHeight: controlHeightLG * 1.4,
pickerCellPaddingVertical: paddingXXS, pickerCellPaddingVertical: paddingXXS + paddingXXS / 2,
pickerCellBorderGap: 2, // Magic for gap between cells pickerCellBorderGap: 2, // Magic for gap between cells
pickerControlIconSize: 7, pickerControlIconSize: 7,
pickerControlIconBorderWidth: 1.5, pickerControlIconBorderWidth: 1.5,
pickerDatePanelPaddingHorizontal: padding + paddingXXS / 2, // 18 in normal
}; };
}; };

7
components/locale/fr_FR.ts

@ -1,8 +1,8 @@
/* eslint-disable no-template-curly-in-string */ /* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/fr_FR'; import Pagination from 'rc-pagination/lib/locale/fr_FR';
import type { Locale } from '.';
import Calendar from '../calendar/locale/fr_FR'; import Calendar from '../calendar/locale/fr_FR';
import DatePicker from '../date-picker/locale/fr_FR'; import DatePicker from '../date-picker/locale/fr_FR';
import type { Locale } from '.';
import TimePicker from '../time-picker/locale/fr_FR'; import TimePicker from '../time-picker/locale/fr_FR';
const typeTemplate = "La valeur du champ ${label} n'est pas valide pour le type ${type}"; const typeTemplate = "La valeur du champ ${label} n'est pas valide pour le type ${type}";
@ -37,6 +37,11 @@ const localeValues: Locale = {
cancelText: 'Annuler', cancelText: 'Annuler',
justOkText: 'OK', justOkText: 'OK',
}, },
Tour: {
Next: 'Étape suivante',
Previous: 'Étape précédente',
Finish: 'Fin de la visite guidée',
},
Popconfirm: { Popconfirm: {
okText: 'OK', okText: 'OK',
cancelText: 'Annuler', cancelText: 'Annuler',

21
components/radio/__tests__/radio.test.tsx

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import Radio, { Button, Group } from '..'; import Radio, { Button, Group } from '..';
import Form from '../../form';
import focusTest from '../../../tests/shared/focusTest'; import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
@ -33,4 +34,24 @@ describe('Radio', () => {
fireEvent.mouseLeave(container.querySelector('label')!); fireEvent.mouseLeave(container.querySelector('label')!);
expect(onMouseLeave).toHaveBeenCalled(); expect(onMouseLeave).toHaveBeenCalled();
}); });
it('should use own disabled status first', () => {
const { getByRole } = render(
<Form disabled>
<Radio disabled={false} />
</Form>,
);
expect(getByRole('radio')).not.toBeDisabled();
});
it('should obtained correctly disabled status', () => {
const { getByRole } = render(
<Form disabled>
<Radio.Group disabled={false}>
<Radio />
</Radio.Group>
</Form>,
);
expect(getByRole('radio')).not.toBeDisabled();
});
}); });

6
components/radio/radio.tsx

@ -33,7 +33,6 @@ const InternalRadio: React.ForwardRefRenderFunction<HTMLElement, RadioProps> = (
rootClassName, rootClassName,
children, children,
style, style,
disabled: customDisabled,
...restProps ...restProps
} = props; } = props;
const radioPrefixCls = getPrefixCls('radio', customizePrefixCls); const radioPrefixCls = getPrefixCls('radio', customizePrefixCls);
@ -49,14 +48,15 @@ const InternalRadio: React.ForwardRefRenderFunction<HTMLElement, RadioProps> = (
// ===================== Disabled ===================== // ===================== Disabled =====================
const disabled = React.useContext(DisabledContext); const disabled = React.useContext(DisabledContext);
radioProps.disabled = customDisabled || disabled;
if (groupContext) { if (groupContext) {
radioProps.name = groupContext.name; radioProps.name = groupContext.name;
radioProps.onChange = onChange; radioProps.onChange = onChange;
radioProps.checked = props.value === groupContext.value; radioProps.checked = props.value === groupContext.value;
radioProps.disabled = radioProps.disabled || groupContext.disabled; radioProps.disabled = radioProps.disabled ?? groupContext.disabled;
} }
radioProps.disabled = radioProps.disabled ?? disabled;
const wrapperClassString = classNames( const wrapperClassString = classNames(
`${prefixCls}-wrapper`, `${prefixCls}-wrapper`,
{ {

30
components/skeleton/style/index.tsx

@ -10,10 +10,10 @@ export type ComponentToken = {
const skeletonClsLoading = new Keyframes(`ant-skeleton-loading`, { const skeletonClsLoading = new Keyframes(`ant-skeleton-loading`, {
'0%': { '0%': {
transform: 'translateX(-37.5%)', backgroundPosition: '100% 50%',
}, },
'100%': { '100%': {
transform: 'translateX(37.5%)', backgroundPosition: '0 50%',
}, },
}); });
@ -45,26 +45,12 @@ const genSkeletonElementAvatarSize = (size: number): CSSObject => ({
}); });
const genSkeletonColor = (token: SkeletonToken): CSSObject => ({ const genSkeletonColor = (token: SkeletonToken): CSSObject => ({
position: 'relative', background: token.skeletonLoadingBackground,
// fix https://github.com/ant-design/ant-design/issues/36444 backgroundSize: '400% 100%',
// https://monshin.github.io/202109/css/safari-border-radius-overflow-hidden/ animationName: skeletonClsLoading,
/* stylelint-disable-next-line property-no-vendor-prefix,value-no-vendor-prefix */ animationDuration: token.skeletonLoadingMotionDuration,
zIndex: 0, animationTimingFunction: 'ease',
overflow: 'hidden', animationIterationCount: 'infinite',
background: 'transparent',
'&::after': {
position: 'absolute',
top: 0,
insetInlineEnd: '-150%',
bottom: 0,
insetInlineStart: '-150%',
background: token.skeletonLoadingBackground,
animationName: skeletonClsLoading,
animationDuration: token.skeletonLoadingMotionDuration,
animationTimingFunction: 'ease',
animationIterationCount: 'infinite',
content: '""',
},
}); });
const genSkeletonElementInputSize = (size: number): CSSObject => ({ const genSkeletonElementInputSize = (size: number): CSSObject => ({

1
components/tree-select/index.en-US.md

@ -51,6 +51,7 @@ Tree selection control.
| loadData | Load data asynchronously | function(node) | - | | | loadData | Load data asynchronously | function(node) | - | |
| maxTagCount | Max tag count to show. `responsive` will cost render performance | number \| `responsive` | - | responsive: 4.10 | | maxTagCount | Max tag count to show. `responsive` will cost render performance | number \| `responsive` | - | responsive: 4.10 |
| maxTagPlaceholder | Placeholder for not showing tags | ReactNode \| function(omittedValues) | - | | | maxTagPlaceholder | Placeholder for not showing tags | ReactNode \| function(omittedValues) | - | |
| maxTagTextLength | Max tag text length to show | number | - | |
| multiple | Support multiple or not, will be `true` when enable `treeCheckable` | boolean | false | | | multiple | Support multiple or not, will be `true` when enable `treeCheckable` | boolean | false | |
| notFoundContent | Specify content to show when no result matches | ReactNode | `Not Found` | | | notFoundContent | Specify content to show when no result matches | ReactNode | `Not Found` | |
| placeholder | Placeholder of the select input | string | - | | | placeholder | Placeholder of the select input | string | - | |

1
components/tree-select/index.zh-CN.md

@ -52,6 +52,7 @@ demo:
| loadData | 异步加载数据 | function(node) | - | | | loadData | 异步加载数据 | function(node) | - | |
| maxTagCount | 最多显示多少个 tag,响应式模式会对性能产生损耗 | number \| `responsive` | - | responsive: 4.10 | | maxTagCount | 最多显示多少个 tag,响应式模式会对性能产生损耗 | number \| `responsive` | - | responsive: 4.10 |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | | | maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | |
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | |
| multiple | 支持多选(当设置 treeCheckable 时自动变为 true) | boolean | false | | | multiple | 支持多选(当设置 treeCheckable 时自动变为 true) | boolean | false | |
| notFoundContent | 当下拉列表为空时显示的内容 | ReactNode | `Not Found` | | | notFoundContent | 当下拉列表为空时显示的内容 | ReactNode | `Not Found` | |
| placeholder | 选择框默认文字 | string | - | | | placeholder | 选择框默认文字 | string | - | |

127
docs/blog/tooltip-align.en-US.md

@ -0,0 +1,127 @@
---
title: Tooltip align update
date: 2023-02-15
author: zombieJ
---
In the `5.3.0` version, we will update the underlying dependency `@rc-component/trigger` of the Tooltip component to better implement adaptive alignment logic. Before that, let's talk about some problems encountered in the previous version.
### About Scroll
Tooltip is append to `body` by default, and it will scroll along with it when scrolling in full screen. But when the target element of the Tooltip is placed in the scrolling container, it will not follow the scrolling because the scrolling container is different:
[<img height="300" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*IkOIQK69T5gAAAAAAAAAAAAADrJ8AQ/original" />](https://codesandbox.io/s/ji-ben-antd-5-2-0-forked-mo31wr?file=/demo.tsx)
We suggest to use `getPopupContainer` in FAQ, allowing developers to insert the popup element into the parent container of the target element through this method. But this solution is not perfect, because it requires the developer to determine which of the parent containers of the target element is the scrolling container. In a reused component, the component that uses the Tooltip may not be the same as the component it scrolls, which makes it not easy to set the target scroll container.
### About Margining
Tooltip supports edge display within the scrolling range. But because the pop-up layer is a whole, the centered arrow cannot point to the target position after offset:
[<img height="160" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*E3RmTZG3vhQAAAAAAAAAAAAADrJ8AQ/original" />](https://codesandbox.io/s/ji-ben-antd-5-2-0-forked-z6frnr?file=/demo.tsx)
We recommend using the `placement` property and configure `topLeft` to align the popup layer to the left to solve this problem before:
<img height="160" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cm4VT5xwBbAAAAAAAAAAAAAADrJ8AQ/original" />
Similarly, if it is a reused component. Maybe it doesn't always need to be displayed side-by-side, it will be very strange when the popup layer is indeed left/right aligned when an element is displayed in the middle.
### About Scale
Tooltip uses the `dom-align` library for align, which will directly add `left` | `top` | `transform` styles to the dom node to achieve alignment, so in order to make it support the React life cycle, we encapsulated it on top of it `rc-align` component. In addition, it only cares about the alignment implementation, not the trigger timing itself. So the `rc-align` component will additionally add a ResizeObserver to monitor size changes, and then call `dom-align` for alignment.
`dom-align` calculates the respective coordinate positions of the target element and the pop-up layer by traversing the parent layer nodes, and then calculates the difference according to the alignment rules. When the parent layer node has a `transform` style, it will cause the calculated coordinate position to be inaccurate, resulting in incorrect alignment:
[<img height="200" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*CVysTIq-y6UAAAAAAAAAAAAADrJ8AQ/original" />](https://codesandbox.io/s/ji-ben-antd-5-2-0-forked-znqgc6?file=/demo.tsx)
## New Align Way
The above problems such as scrolling and margining can be avoided in some ways, but the scaling problem cannot be solved. We hope that these problems can be solved by antd, rather than by the developers themselves. To this end, we rewrote the `@rc-component/trigger` component to integrate alignment logic and arrow logic. No longer depend on `rc-align` and `dom-align`. At the same time, use the new calculation method to avoid calculation problems caused by the `transform` style.
### Position Calculation
Considering that there are various `position`s in the parent node of the popup element, it is not cost-effective to recursively search the parent element node to calculate the relative position. We only need to calculate the offset according to the final positions of the two, and then apply the final zoom ratio of the popup layer:
1. Generate the Popup element
2. Add the Popup style `left: 0` & `top: 0` to force it to be aligned to the upper left corner
- There may be `fixed`, `relative`, and `absolute` nodes in `position` in the parent container of the Popup element, which does not affect our calculation of offset. Just make sure to make an offset at the `0/0` position
3. Obtain the position information of the target element and Popup element through `getBoundingClientRect`
4. Calculate the offset difference
<img alt="Popup Align" height="300" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*mF3RQZF1w7sAAAAAAAAAAAAADrJ8AQ/original" />
### Scale
The zoom ratio cannot be obtained directly, but we can calculate the zoom ratio through `getBoundingClientRect` and `offsetWidth`/`offsetHeight`:
```tsx
const popupRect = popupEle.getBoundingClientRect();
const { offsetWidth, offsetHeight } = popupEle;
const scaleX = popupRect.width / offsetWidth;
const scaleY = popupRect.height / offsetHeight;
```
Then apply the scaling to the calculated offset:
```tsx
// Some logic for align offset calculation
// const baseOffsetX = ...
// const baseOffsetY = ...
const scaledOffsetX = baseOffsetX / scaleX;
const scaledOffsetY = baseOffsetY / scaleY;
```
### Arrow
In the past, arrows were added by `rc-tooltip` instead of `rc-trigger`. This makes the `rc-tooltip` lost the alignment information, so that the arrow position cannot be adjusted correctly when the Popup is offset. To this end, we also integrate the arrow logic into `rc-trigger`, so that the position of the arrow can be offset with the offset of the Popup. After merging, the arrow position calculation becomes very simple. We only need to take the minimum value of the target element and the Popup boundary value, and then take the middle value:
#### Center Position
<img alt="center" height="200" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*D3bFRJFAYWwAAAAAAAAAAAAADrJ8AQ/original" />
#### Margining Position
<img alt="center" height="200" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*oe8SQ6d8GVgAAAAAAAAAAAAADrJ8AQ/original" />
### Visible Region
The new monitoring mode will detect the `overflow` style of the Popup parent node when the Tooltip is started. When `scroll`, `hidden`, and `auto` exist, the visible area except the scroll bar will be superimposed to calculate the final display area:
<img alt="VisibleRegion" height="300" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a4rnSY0KJGEAAAAAAAAAAAAADrJ8AQ/original" />
Similarly, we need to listen to its scrolling events. When any parent node is scrolled, the display area needs to be recalculated:
```tsx
function collectScroll(ele: HTMLElement) {
const scrollList: HTMLElement[] = [];
let current = ele?.parentElement;
while (current) {
if (isScrollContainer(current)) {
scrollList.push(ele);
}
current = current.parentElement;
}
return scrollList;
}
const targetScrollList = collectScroll(targetEle);
const popupScrollList = collectScroll(popupEle);
// We merge the list in real world. Here just for sample
[window, ...targetScrollList, ...popupScrollList].forEach((ele) => {
ele.addEventListener(...);
});
```
In the end, we get the effect of adaptive scrolling:
<img alt="scroll" height="300" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*sxaTTJjLtIMAAAAAAAAAAAAADrJ8AQ/original" />
## Finally
After completing the transformation of Tooltip, we will continue to transform other components which has popup element. We hope that after this, developers can use components directly instead of paying attention to the configuration of `getPopupContainer` as much as possible. Have a nice day!

127
docs/blog/tooltip-align.zh-CN.md

@ -0,0 +1,127 @@
---
title: 新的 Tooltip 对齐方式
date: 2023-02-15
author: zombieJ
---
`5.3.0` 版本中,我们将会更新 Tooltip 组件的底层依赖 `@rc-component/trigger` 使其更好的实现自适应对齐逻辑。在此之前,我们先聊聊此前版本遇到的一些问题。
### 滚动问题
Tooltip 默认添加至 `body` 上,在全屏滚动时它会随着一起滚动。但是当 Tooltip 的目标元素放置在滚动容器中,则因为滚动容器不同而出现不会跟随滚动的情况:
[<img height="300" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*IkOIQK69T5gAAAAAAAAAAAAADrJ8AQ/original" />](https://codesandbox.io/s/ji-ben-antd-5-2-0-forked-mo31wr?file=/demo.tsx)
我们集中在 FAQ 提供了 `getPopupContainer` 的方式,让开发者将弹出元素通过该方法插入到目标元素的父级容器中,从而解决这个问题。但是这个方案并不完美,因为它需要开发者自己去判断目标元素的父级容器中哪个是滚动容器。在多层封装的组件中,可能使用 Tooltip 的组件与其滚动的组件并不是同一个,这使得设置目标滚动容器并不容易。
### 贴边问题
Tooltip 支持在滚动范围内贴边展示。但是由于弹出层是整体,使得居中的箭头在偏移后无法指向目标位置:
[<img height="160" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*E3RmTZG3vhQAAAAAAAAAAAAADrJ8AQ/original" />](https://codesandbox.io/s/ji-ben-antd-5-2-0-forked-z6frnr?file=/demo.tsx)
我们会推荐使用 `placement` 属性,配置 `topLeft` 让弹出层靠左对齐来解决这个问题:
<img height="160" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*cm4VT5xwBbAAAAAAAAAAAAAADrJ8AQ/original" />
同样的,如果是复用组件。可能它并不是总是需要贴边展示,当一个元素在中间展示时弹出层确实左/右对齐就会显得非常奇怪。
### 缩放问题
Tooltip 对齐底层使用 `dom-align` 库,它会直接为 dom 节点添加 `left` | `top` | `transform` 样式来实现对齐,因而为了使其支持 React 生命周期,我们在此之上封装了 `rc-align` 组件。此外,它只关注对齐实现,本身不关注触发时机。所以 `rc-align` 组件还会额外添加 ResizeObserver 监听尺寸变化,继而调用 `dom-align` 进行对齐。
`dom-align` 通过遍历父层节点累加计算出目标元素和弹出层各自的坐标位置,接着根据对齐规则计算差值。当父层节点有 `transform` 样式时,会导致计算出的坐标位置不准确,从而导致对齐不正确:
[<img height="200" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*CVysTIq-y6UAAAAAAAAAAAAADrJ8AQ/original" />](https://codesandbox.io/s/ji-ben-antd-5-2-0-forked-znqgc6?file=/demo.tsx)
## 新的对齐方式
以上问题如滚动、贴边可以使用一些方式规避,而缩放问题则无法解决。我们希望这些问题可以由 antd 底层来解,而不是由开发者自己去处理。为此,我们重写了 `@rc-component/trigger` 组件,将对齐逻辑和箭头逻辑整合在一起。不再依赖 `rc-align` 以及 `dom-align`。同时使用新的计算方式避免 `transform` 样式导致的计算问题。
### 位置计算
考虑到弹出元素父层节点存在各种 `position` 的情况,递归查找父元素节点计算相对位置并不划算。我们只需要根据两者最终位置做偏移计算,再应用弹出层最终的缩放比例即可:
1. 生成 Popup 元素
2. 添加 Popup 样式 `left: 0` & `top: 0`,将其强制对齐到左上角
- Popup 元素父容器中 `position` 可能存在 `fixed`、`relative`、`absolute`节点,这都不影响我们计算偏移量。只要保证在 `0/0` 位置做偏移即可
3. 通过 `getBoundingClientRect` 获取目标元素和 Popup 元素的位置信息
4. 计算偏移差值
<img alt="Popup Align" height="300" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*mF3RQZF1w7sAAAAAAAAAAAAADrJ8AQ/original" />
### 缩放比
缩放比例无法直接获取,但是我们可以通过 `getBoundingClientRect``offsetWidth`/`offsetHeight` 计算出缩放比例:
```tsx
const popupRect = popupEle.getBoundingClientRect();
const { offsetWidth, offsetHeight } = popupEle;
const scaleX = popupRect.width / offsetWidth;
const scaleY = popupRect.height / offsetHeight;
```
接着将缩放比例应用到计算出来的偏移值即可:
```tsx
// Some logic for align offset calculation
// const baseOffsetX = ...
// const baseOffsetY = ...
const scaledOffsetX = baseOffsetX / scaleX;
const scaledOffsetY = baseOffsetY / scaleY;
```
### 箭头优化
在过去版本中,箭头由 `rc-tooltip` 添加而非 `rc-trigger` 管理。这使得 `rc-tooltip` 封装时已经丢失了对齐信息,以至于无法在 Popup 偏移时正确的调整箭头位置。为此,我们将箭头逻辑也整合到 `rc-trigger` 中,使得箭头的位置可以随着 Popup 的偏移而偏移。合并之后,箭头位置计算变得十分简单。我们只要取目标元素和 Popup 边界值最小值,再取中间值即可:
#### 居中定位
<img alt="center" height="200" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*D3bFRJFAYWwAAAAAAAAAAAAADrJ8AQ/original" />
#### 贴边定位
<img alt="center" height="200" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*oe8SQ6d8GVgAAAAAAAAAAAAADrJ8AQ/original" />
### 显示区域计算
新的监听模式会在启动 Tooltip 时检测 Popup 父节点 `overflow` 样式,当存在 `scroll`、`hidden`、`auto` 时,叠加除滚动条外的可见区域,从而计算出最终的显示区域:
<img alt="VisibleRegion" height="300" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*a4rnSY0KJGEAAAAAAAAAAAAADrJ8AQ/original" />
同样的,我们需要监听其滚动事件。任意父层节点滚动时,都需要重新计算显示区域:
```tsx
function collectScroll(ele: HTMLElement) {
const scrollList: HTMLElement[] = [];
let current = ele?.parentElement;
while (current) {
if (isScrollContainer(current)) {
scrollList.push(ele);
}
current = current.parentElement;
}
return scrollList;
}
const targetScrollList = collectScroll(targetEle);
const popupScrollList = collectScroll(popupEle);
// We merge the list in real world. Here just for sample
[window, ...targetScrollList, ...popupScrollList].forEach((ele) => {
ele.addEventListener(...);
});
```
最终,就得到了可以自适应滚动的效果:
<img alt="scroll" height="300" src="https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*sxaTTJjLtIMAAAAAAAAAAAAADrJ8AQ/original" />
## 总结
在完成 Tooltip 改造后,我们还会继续改造其他使用到弹出层的组件。希望在此之后,开发者可以尽可能不需要再关注 `getPopupContainer` 的配置,而是可以直接使用组件。祝你开发愉快!

2
docs/spec/cases.en-US.md

@ -24,7 +24,7 @@ Cloud-oriented financial services, used by financial institutions that benefit f
OceanBase Cloud is a distributed relational database in a real sense, and OceanBase Cloud Platform is the OceanBase cloud-based database service that can help users quickly create and use OceanBase service. OceanBase Cloud is a distributed relational database in a real sense, and OceanBase Cloud Platform is the OceanBase cloud-based database service that can help users quickly create and use OceanBase service.
[Visit](<[http://oceanbase.alipay.com](https://www.oceanbase.com/docs/enterprise/oceanbase-ocp-cn/V3.3.3/10000000000636101)>) [Visit](https://en.oceanbase.com/docs/)
![OceanBase Cloud Platform](https://gw.alipayobjects.com/zos/rmsportal/OYGCAlMwSWkdaKfxIDtz.png) ![OceanBase Cloud Platform](https://gw.alipayobjects.com/zos/rmsportal/OYGCAlMwSWkdaKfxIDtz.png)

2
docs/spec/cases.zh-CN.md

@ -22,7 +22,7 @@ Ant Design 目前在外部也有许多产品实践,如果你的公司和产品
OceanBase 是一款真正意义上的云端分布式关系型数据库,而 OceanBase Cloud Platform(云平台)是以 OceanBase 数据库为基础的云服务,可以帮助用户快速创建、使用 OB 服务。 OceanBase 是一款真正意义上的云端分布式关系型数据库,而 OceanBase Cloud Platform(云平台)是以 OceanBase 数据库为基础的云服务,可以帮助用户快速创建、使用 OB 服务。
[立即访问](https://www.oceanbase.com/docs/enterprise/oceanbase-ocp-cn/V3.3.3/10000000000636101) [立即访问](https://www.oceanbase.com/docs/)
![OceanBase 云平台](https://gw.alipayobjects.com/zos/rmsportal/OYGCAlMwSWkdaKfxIDtz.png) ![OceanBase 云平台](https://gw.alipayobjects.com/zos/rmsportal/OYGCAlMwSWkdaKfxIDtz.png)

8
package.json

@ -87,12 +87,12 @@
"sort": "npx sort-package-json", "sort": "npx sort-package-json",
"sort-api": "antd-tools run sort-api-table", "sort-api": "antd-tools run sort-api-table",
"start": "cross-env PORT=8001 dumi dev", "start": "cross-env PORT=8001 dumi dev",
"test": "jest --config .jest.js --cache=false", "test": "jest --config .jest.js --no-cache",
"test:update": "jest --config .jest.js --cache=false -u", "test:update": "jest --config .jest.js --no-cache -u",
"test-all": "sh -e ./scripts/test-all.sh", "test-all": "sh -e ./scripts/test-all.sh",
"test-node": "npm run version && jest --config .jest.node.js --cache=false", "test-node": "npm run version && jest --config .jest.node.js --no-cache",
"tsc": "tsc --noEmit", "tsc": "tsc --noEmit",
"site:test": "jest --config .jest.site.js --cache=false --force-exit", "site:test": "jest --config .jest.site.js --no-cache --force-exit",
"test-image": "jest --config .jest.image.js --no-cache -i -u", "test-image": "jest --config .jest.image.js --no-cache -i -u",
"argos": "node ./scripts/argos-upload.js", "argos": "node ./scripts/argos-upload.js",
"version": "node ./scripts/generate-version", "version": "node ./scripts/generate-version",

Loading…
Cancel
Save