diff --git a/src/components/cascader-view/tests/__snapshots__/cascader-view.test.tsx.snap b/src/components/cascader-view/tests/__snapshots__/cascader-view.test.tsx.snap index fc064bb7bb..328e460330 100644 --- a/src/components/cascader-view/tests/__snapshots__/cascader-view.test.tsx.snap +++ b/src/components/cascader-view/tests/__snapshots__/cascader-view.test.tsx.snap @@ -7,6 +7,7 @@ exports[`CascaderView basic usage 1`] = ` >
{ + const [rtl, setRtl] = React.useState(true) + const [activeLineMode, setActiveLineMode] = + React.useState('auto') + + return ( + + { + setRtl(v) + }} + /> + + activeLineMode: + { + setActiveLineMode(v as TabsProps['activeLineMode']) + }} + > + + {modes.map(mode => ( + + {mode} + + ))} + + + + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + + + ) +} diff --git a/src/components/tabs/index.en.md b/src/components/tabs/index.en.md index d6e34ab092..182c8f6ddd 100644 --- a/src/components/tabs/index.en.md +++ b/src/components/tabs/index.en.md @@ -16,6 +16,8 @@ The current content needs to be divided into groups of the same hierarchical str + + ## Tabs ### Props @@ -27,6 +29,7 @@ The current content needs to be divided into groups of the same hierarchical str | defaultActiveKey | The initialized `key` of the selected panel, if the `activeKey` is not set | `string \| null` | the `key` of the 1st pannel | | onChange | Callback when switching panel | `(key: string) => void` | - | | stretch | Whether stretch the tab header | `boolean` | `true` | +| direction | Document layout direction | `'ltr' \| 'rtl'` | `'ltr'` | ### CSS Variables diff --git a/src/components/tabs/index.zh.md b/src/components/tabs/index.zh.md index df3f840232..f682e0b6c9 100644 --- a/src/components/tabs/index.zh.md +++ b/src/components/tabs/index.zh.md @@ -16,6 +16,8 @@ + + ## Tabs ### 属性 @@ -27,6 +29,7 @@ | defaultActiveKey | 初始化选中面板的 `key`,如果没有设置 `activeKey` | `string \| null` | 第一个面板的 `key` | | onChange | 切换面板的回调 | `(key: string) => void` | - | | stretch | 选项卡头部是否拉伸 | `boolean` | `true` | +| direction | 文档排版方向 | `'ltr' \| 'rtl'` | `'ltr'` | ### CSS 变量 diff --git a/src/components/tabs/tabs.tsx b/src/components/tabs/tabs.tsx index 3301f15ef0..026457cb21 100644 --- a/src/components/tabs/tabs.tsx +++ b/src/components/tabs/tabs.tsx @@ -39,6 +39,7 @@ export type TabsProps = { stretch?: boolean onChange?: (key: string) => void children?: React.ReactNode + direction?: 'ltr' | 'rtl' } & NativeProps< | '--fixed-active-line-width' | '--active-line-height' @@ -52,6 +53,7 @@ export type TabsProps = { const defaultProps = { activeLineMode: 'auto', stretch: true, + direction: 'ltr', } export const Tabs: FC = p => { @@ -63,6 +65,8 @@ export const Tabs: FC = p => { const panes: ReactElement[] = [] + const isRTL = props.direction === 'rtl' + traverseReactNode(props.children, (child, index) => { if (!isValidElement(child)) return @@ -150,6 +154,19 @@ export const Tabs: FC = p => { } else { x = activeTabLeft + (activeTabWidth - activeLineWidth) / 2 } + + if (isRTL) { + /** + * In RTL mode, x equals the container width minus the x-coordinate of the current tab minus the width of the current tab. + * https://github.com/Fog3211/reproduce-codesandbox/blob/f0a3396a114cc00e88a51a67d3be60a746519b30/assets/images/antd_mobile_tabs_rtl_x.jpg?raw=true + */ + const w = + props.activeLineMode === 'auto' || props.activeLineMode === 'full' + ? width + : activeLineWidth + x = -(containerWidth - x - w) + } + api.start({ x, width, @@ -159,12 +176,27 @@ export const Tabs: FC = p => { const maxScrollDistance = containerScrollWidth - containerWidth if (maxScrollDistance <= 0) return - const nextScrollLeft = bound( + let nextScrollLeft = bound( activeTabLeft - (containerWidth - activeTabWidth) / 2, 0, - containerScrollWidth - containerWidth + maxScrollDistance ) + if (isRTL) { + /** + * 位移距离等于:activeTab的中心坐标距离容器中心坐标的距离,然后RTL取负数 + * containerWidth / 2 - (activeTabLeft + (activeTabWidth - activeLineWidth) / 2) - activeLineWidth / 2, + */ + nextScrollLeft = -bound( + containerWidth / 2 - + activeTabLeft + + activeTabWidth / 2 - + activeLineWidth, + 0, + maxScrollDistance + ) + } + scrollApi.start({ scrollLeft: nextScrollLeft, from: { scrollLeft: containerScrollLeft }, @@ -202,9 +234,25 @@ export const Tabs: FC = p => { if (!container) return const scrollLeft = container.scrollLeft - const showLeftMask = scrollLeft > 0 - const showRightMask = - scrollLeft + container.offsetWidth < container.scrollWidth + + let showLeftMask = false + let showRightMask = false + + if (isRTL) { + /** + * RTL模式下,只要滑动过,scrollLeft就再也回不到0(chrome是0.5) + * 所以要加round才能终止触发条件 + * round(443.5) + 375 < 819 + */ + showLeftMask = + Math.round(-scrollLeft) + container.offsetWidth < + container.scrollWidth + showRightMask = scrollLeft < 0 + } else { + showLeftMask = scrollLeft > 0 + showRightMask = + scrollLeft + container.offsetWidth < container.scrollWidth + } maskApi.start({ leftMaskOpacity: showLeftMask ? 1 : 0, @@ -225,7 +273,12 @@ export const Tabs: FC = p => { return withNativeProps( props, -
+