diff --git a/src/config.json b/src/config.json index c87db02beb..b1370e7c5c 100644 --- a/src/config.json +++ b/src/config.json @@ -480,7 +480,7 @@ "v15": 2, "author": "oasis", "dd": true, - "v16": false + "v16": true } ] }, diff --git a/src/packages/configprovider/types.ts b/src/packages/configprovider/types.ts index b6cb31409c..a191d9e64b 100644 --- a/src/packages/configprovider/types.ts +++ b/src/packages/configprovider/types.ts @@ -738,10 +738,13 @@ export type NutCSSVariables = | 'nutuiTabsTitlesBackgroundColor' | 'nutuiTabsTitlesGap' | 'nutuiTabsTitlesFontSize' + | 'nutuiTabsTitlesItemFontFamily' + | 'nutuiTabsTitlesItemFontWeight' | 'nutuiTabsTitlesItemMinWidth' | 'nutuiTabsTitlesItemColor' | 'nutuiTabsTitlesItemActiveColor' | 'nutuiTabsTitlesItemActiveFontWeight' + | 'nutuiTabsTitlesItemActiveFontFamily' | 'nutuiTabsTitlesItemActiveFontSize' | 'nutuiTabsTitlesItemActiveBackgroundColor' | 'nutuiTabsTabLineWidth' @@ -751,6 +754,10 @@ export type NutCSSVariables = | 'nutuiTabsLineBorderRadius' | 'nutuiTabsTabLineOpacity' | 'nutuiTabsButtonBorderRadius' + | 'nutuiTabsButtonFontSize' + | 'nutuiTabsButtonActiveFontSize' + | 'nutuiTabsButtonActiveFontWeight' + | 'nutuiTabsButtonBackgroundColor' | 'nutuiTabsButtonActiveBackgroundColor' | 'nutuiTabsButtonActiveBorder' | 'nutuiTabsVerticalTitlesWidth' diff --git a/src/packages/tabs/__test__/tabs.spec.tsx b/src/packages/tabs/__test__/tabs.spec.tsx index 69a02517da..787903c614 100644 --- a/src/packages/tabs/__test__/tabs.spec.tsx +++ b/src/packages/tabs/__test__/tabs.spec.tsx @@ -3,6 +3,7 @@ import { render, waitFor, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom' import { Tabs } from '../tabs' import { TabPane } from '../../tabpane/tabpane' +import { getCardFlushSides } from '../utils' test('base Tabs', () => { const { getByTestId } = render( @@ -26,7 +27,89 @@ test('base tabs props', () => { expect(el2.length > 0).toBe(true) expect(el3).toHaveClass('nut-tabs-titles-smile') - expect(el3).toHaveClass('nut-tabs-titles-scrollable') + expect(el3).not.toHaveClass('nut-tabs-titles-scrollable') +}) + +test('card flush sides follow scrollable, align and rtl', () => { + // 可滑动(未设置 align):首项贴物理左端、末项贴物理右端 + expect(getCardFlushSides(0, 3, { scrollable: true, rtl: false })).toEqual({ + left: true, + right: false, + }) + expect(getCardFlushSides(2, 3, { scrollable: true, rtl: false })).toEqual({ + left: false, + right: true, + }) + expect(getCardFlushSides(1, 3, { scrollable: true, rtl: false })).toEqual({ + left: false, + right: false, + }) + // RTL 镜像:首项贴物理右端、末项贴物理左端 + expect(getCardFlushSides(0, 3, { scrollable: true, rtl: true })).toEqual({ + left: false, + right: true, + }) + expect(getCardFlushSides(2, 3, { scrollable: true, rtl: true })).toEqual({ + left: true, + right: false, + }) + // 不可滑动:内容靠向 align 一侧,只有贴靠该侧的那一项才贴边 + expect( + getCardFlushSides(0, 3, { scrollable: false, rtl: false, align: 'left' }) + ).toEqual({ left: true, right: false }) + expect( + getCardFlushSides(2, 3, { scrollable: false, rtl: false, align: 'left' }) + ).toEqual({ left: false, right: false }) + expect( + getCardFlushSides(2, 3, { scrollable: false, rtl: false, align: 'right' }) + ).toEqual({ left: false, right: true }) + expect( + getCardFlushSides(0, 3, { scrollable: false, rtl: true, align: 'left' }) + ).toEqual({ left: false, right: true }) + expect( + getCardFlushSides(2, 3, { scrollable: false, rtl: true, align: 'right' }) + ).toEqual({ left: true, right: false }) + // 单项卡片两端同时贴边 + expect(getCardFlushSides(0, 1, { scrollable: true, rtl: false })).toEqual({ + left: true, + right: true, + }) +}) + +test('card tabs apply flush modifier to active item', () => { + const { container } = render( + + + Tab 1 + + + Tab 2 + + + ) + const titles = container.querySelectorAll('.nut-tabs-titles')[0] + expect(titles).not.toHaveClass('nut-tabs-titles-scrollable') + expect(titles).not.toHaveClass('nut-tabs-titles-not-scrollable') + + const items = container.querySelectorAll('.nut-tabs-titles-item') + expect(items[0]).toHaveClass('nut-tabs-titles-item-active--flush-left') + expect(items[0]).not.toHaveClass('nut-tabs-titles-item-active--flush-right') +}) + +test('card tabs flush modifier honours align when not scrollable', () => { + const { container } = render( + + + Tab 1 + + + Tab 2 + + + ) + const items = container.querySelectorAll('.nut-tabs-titles-item') + expect(items[1]).toHaveClass('nut-tabs-titles-item-active--flush-right') + expect(items[1]).not.toHaveClass('nut-tabs-titles-item-active--flush-left') }) test('base tabs props', () => { @@ -41,6 +124,28 @@ test('base tabs props', () => { expect(el3).toHaveClass('nut-tabs-titles-card') }) +test('button type tabs', () => { + const { container } = render( + + + Tab 1 + + + Tab 2 + + + ) + const titles = container.querySelectorAll('.nut-tabs-titles')[0] + expect(titles).toHaveClass('nut-tabs-titles-button') + + const items = container.querySelectorAll('.nut-tabs-titles-item') + const texts = container.querySelectorAll('.nut-tabs-titles-item-text') + expect(items[0]).toHaveClass('nut-tabs-titles-item-active') + expect(items[1]).not.toHaveClass('nut-tabs-titles-item-active') + expect(texts.length).toBe(2) + expect(texts[0]).toHaveTextContent('Tab 1') +}) + test('base other props', async () => { const { container } = render( diff --git a/src/packages/tabs/demo.taro.tsx b/src/packages/tabs/demo.taro.tsx index fc5dd95801..70fa174123 100644 --- a/src/packages/tabs/demo.taro.tsx +++ b/src/packages/tabs/demo.taro.tsx @@ -28,6 +28,8 @@ import Demo20 from './demos/taro/demo20' import Demo21 from './demos/taro/demo21' import Demo22 from './demos/taro/demo22' import Demo23 from './demos/taro/demo23' +import Demo24 from './demos/taro/demo24' +import Demo25 from './demos/taro/demo25' const TabsDemo = () => { const [translated] = useTranslate({ @@ -49,6 +51,8 @@ const TabsDemo = () => { title14: '滑动切换', title7: 'Title 字体尺寸:20px 12px', title8: '自定义标签栏', + title15: '自定义标签栏-辅助信息', + title16: '左对齐-辅助操作', title9: 'Tabpane 自动高度', title10: 'CSS 粘性布局', title11: 'Title 左对齐', @@ -74,6 +78,8 @@ const TabsDemo = () => { title14: 'Slide To Switch', title7: 'Title FontSize: 20px 12px', title8: 'Custom Tab Bar', + title15: 'Custom Tab Bar - Auxiliary Info', + title16: 'Left Align - Auxiliary Operation', title9: 'Tabpane Auto Height', title10: 'CSS Sticky', title11: 'Title Left Align', @@ -140,6 +146,10 @@ const TabsDemo = () => { {translated.title8} + {translated.title15} + + {translated.title16} + ) diff --git a/src/packages/tabs/demo.tsx b/src/packages/tabs/demo.tsx index c43e4c51c2..1049710ff3 100644 --- a/src/packages/tabs/demo.tsx +++ b/src/packages/tabs/demo.tsx @@ -23,6 +23,8 @@ import Demo20 from './demos/h5/demo20' import Demo21 from './demos/h5/demo21' import Demo22 from './demos/h5/demo22' import Demo23 from './demos/h5/demo23' +import Demo24 from './demos/h5/demo24' +import Demo25 from './demos/h5/demo25' const TabsDemo = () => { const [translated] = useTranslate({ @@ -43,6 +45,8 @@ const TabsDemo = () => { title14: '滑动切换', title7: 'Title 字体尺寸:20px 12px', title8: '自定义标签栏', + title15: '自定义标签栏-辅助信息', + title16: '左对齐-辅助操作', title9: 'Tabpane 自动高度', title10: 'CSS 粘性布局', title11: 'Title 左对齐', @@ -67,6 +71,8 @@ const TabsDemo = () => { title14: 'Slide To Switch', title7: 'Title FontSize: 20px 12px', title8: 'Custom Tab Bar', + title15: 'Custom Tab Bar - Auxiliary Info', + title16: 'Left Align - Auxiliary Operation', title9: 'Tabpane Auto Height', title10: 'CSS Sticky', title11: 'Title Left Align', @@ -125,6 +131,10 @@ const TabsDemo = () => {

{translated.title8}

+

{translated.title15}

+ +

{translated.title16}

+ ) diff --git a/src/packages/tabs/demos/h5/demo22.tsx b/src/packages/tabs/demos/h5/demo22.tsx index a06e85e072..a9051bf676 100644 --- a/src/packages/tabs/demos/h5/demo22.tsx +++ b/src/packages/tabs/demos/h5/demo22.tsx @@ -11,7 +11,10 @@ const Demo22 = () => { onChange={(value) => { setTabvalue(value) }} - style={{ '--nutui-tabs-titles-font-size': '20px' }} + style={{ + '--nutui-tabs-titles-font-size': + 'calc(20px * var(--nut-scale-font, 1))', + }} > Tab longitem Tab 2 @@ -22,7 +25,10 @@ const Demo22 = () => { onChange={(value) => { setTabvalue1(value) }} - style={{ '--nutui-tabs-titles-font-size': '12px' }} + style={{ + '--nutui-tabs-titles-font-size': + 'calc(12px * var(--nut-scale-font, 1))', + }} > Tab longitem Tab 2 diff --git a/src/packages/tabs/demos/h5/demo24.tsx b/src/packages/tabs/demos/h5/demo24.tsx new file mode 100644 index 0000000000..86604d84fa --- /dev/null +++ b/src/packages/tabs/demos/h5/demo24.tsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react' +import { Tabs } from '@nutui/nutui-react' + +const Demo24 = () => { + const [tabvalue, setTabvalue] = useState('c1') + const list = [ + { title: '选中态标题', subtitle: '辅助信息文本', paneKey: 'c1' }, + { title: '非选中态标题', subtitle: '辅助信息文本', paneKey: 'c2' }, + { title: '非选中态标题', subtitle: '辅助信息文本', paneKey: 'c3' }, + { title: '非选中态标题', subtitle: '辅助信息文本', paneKey: 'c4' }, + ] + return ( + { + return list.map((item) => { + const active = tabvalue === item.paneKey + return ( +
setTabvalue(item.paneKey)} + className={`nut-tabs-titles-item ${active ? 'nut-tabs-titles-item-active' : ''}`} + style={{ + flexDirection: 'column', + justifyContent: 'center', + lineHeight: 'initial', + }} + > + {item.title} + + {item.subtitle} + +
+ ) + }) + }} + > + {list.map((item) => ( + + {item.title} + + ))} +
+ ) +} +export default Demo24 diff --git a/src/packages/tabs/demos/h5/demo25.tsx b/src/packages/tabs/demos/h5/demo25.tsx new file mode 100644 index 0000000000..6296a384bf --- /dev/null +++ b/src/packages/tabs/demos/h5/demo25.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react' +import { Tabs } from '@nutui/nutui-react' +import { Search } from '@nutui/icons-react' + +const Demo25 = () => { + const [tabvalue, setTabvalue] = useState('c1') + const list = [ + { title: 'Tab longitem 1', paneKey: 'c1' }, + { title: 'Tab longitem 2', paneKey: 'c2' }, + { title: 'Tab3', paneKey: 'c3' }, + { title: 'Tab4', paneKey: 'c4' }, + { title: 'Tab5', paneKey: 'c5' }, + { title: 'Tab6', paneKey: 'c6' }, + ] + + const renderTitles = () => { + const items = list.map((item) => { + const active = tabvalue === item.paneKey + return ( +
setTabvalue(item.paneKey)} + className={`nut-tabs-titles-item ${active ? 'nut-tabs-titles-item-active' : ''}`} + > + {item.title} + +
+ ) + }) + // 末尾占位:预留 >= 辅助操作宽度的空白,滚到最右端时末项文字不被辅助操作遮挡 + items.push( +
+ ) + return items + } + + return ( +
+ + {list.map((item) => ( + + {item.title} + + ))} + +
+
+
+ +
+
+
+ ) +} +export default Demo25 diff --git a/src/packages/tabs/demos/h5/demo4.tsx b/src/packages/tabs/demos/h5/demo4.tsx index aaca44d0ea..592fe0aee7 100644 --- a/src/packages/tabs/demos/h5/demo4.tsx +++ b/src/packages/tabs/demos/h5/demo4.tsx @@ -7,6 +7,11 @@ const Demo4 = () => { <> { setTabvalue(value) }} diff --git a/src/packages/tabs/demos/h5/demo5.tsx b/src/packages/tabs/demos/h5/demo5.tsx index 9277fcf0fd..9dd70d98ea 100644 --- a/src/packages/tabs/demos/h5/demo5.tsx +++ b/src/packages/tabs/demos/h5/demo5.tsx @@ -11,6 +11,7 @@ const Demo5 = () => { setTabvalue(value) }} activeType="button" + style={{ '--nutui-tabs-titles-item-font-family': 'PingFang SC' }} > Tab longitem Tab 2 diff --git a/src/packages/tabs/demos/h5/demo8.tsx b/src/packages/tabs/demos/h5/demo8.tsx index 7b28f4963c..37fba43131 100644 --- a/src/packages/tabs/demos/h5/demo8.tsx +++ b/src/packages/tabs/demos/h5/demo8.tsx @@ -7,6 +7,11 @@ const Demo8 = () => { <> { diff --git a/src/packages/tabs/demos/taro/demo24.tsx b/src/packages/tabs/demos/taro/demo24.tsx new file mode 100644 index 0000000000..8bd2d14185 --- /dev/null +++ b/src/packages/tabs/demos/taro/demo24.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react' +import { View, Text } from '@tarojs/components' +import { Tabs, pxTransform } from '@nutui/nutui-react-taro' + +const Demo24 = () => { + const [tabvalue, setTabvalue] = useState('c1') + const list = [ + { title: '选中态标题', subtitle: '辅助信息文本', paneKey: 'c1' }, + { title: '非选中态标题', subtitle: '辅助信息文本', paneKey: 'c2' }, + { title: '非选中态标题', subtitle: '辅助信息文本', paneKey: 'c3' }, + { title: '非选中态标题', subtitle: '辅助信息文本', paneKey: 'c4' }, + ] + return ( + { + return list.map((item) => { + const active = tabvalue === item.paneKey + return ( + setTabvalue(item.paneKey)} + className={`nut-tabs-titles-item ${active ? 'nut-tabs-titles-item-active' : ''}`} + style={{ + flexDirection: 'column', + justifyContent: 'center', + lineHeight: 'initial', + }} + > + {item.title} + + {item.subtitle} + + + ) + }) + }} + > + {list.map((item) => ( + + {item.title} + + ))} + + ) +} +export default Demo24 diff --git a/src/packages/tabs/demos/taro/demo25.tsx b/src/packages/tabs/demos/taro/demo25.tsx new file mode 100644 index 0000000000..0b3286fc5d --- /dev/null +++ b/src/packages/tabs/demos/taro/demo25.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react' +import { View, Text } from '@tarojs/components' +import { Tabs, pxTransform } from '@nutui/nutui-react-taro' +import { Search } from '@nutui/icons-react-taro' + +const Demo25 = () => { + const [tabvalue, setTabvalue] = useState('c1') + const list = [ + { title: 'Tab longitem 1', paneKey: 'c1' }, + { title: 'Tab longitem 2', paneKey: 'c2' }, + { title: 'Tab3', paneKey: 'c3' }, + { title: 'Tab4', paneKey: 'c4' }, + { title: 'Tab5', paneKey: 'c5' }, + { title: 'Tab6', paneKey: 'c6' }, + ] + + const renderTitles = () => { + const items = list.map((item) => { + const active = tabvalue === item.paneKey + return ( + setTabvalue(item.paneKey)} + className={`nut-tabs-titles-item ${active ? 'nut-tabs-titles-item-active' : ''}`} + > + {item.title} + + + ) + }) + // 末尾占位:预留 >= 辅助操作宽度的空白,滚到最右端时末项文字不被辅助操作遮挡 + items.push( + + ) + return items + } + + return ( + + + {list.map((item) => ( + + {item.title} + + ))} + + + + + + + + + ) +} +export default Demo25 diff --git a/src/packages/tabs/demos/taro/demo4.tsx b/src/packages/tabs/demos/taro/demo4.tsx index 6c1a3df1ff..72ef13827f 100644 --- a/src/packages/tabs/demos/taro/demo4.tsx +++ b/src/packages/tabs/demos/taro/demo4.tsx @@ -1,11 +1,16 @@ import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react-taro' +import { Tabs, pxTransform } from '@nutui/nutui-react-taro' const Demo4 = () => { const [tabvalue, setTabvalue] = useState('0') return ( { setTabvalue(value) }} diff --git a/src/packages/tabs/demos/taro/demo5.tsx b/src/packages/tabs/demos/taro/demo5.tsx index d116fe27d4..168c99dbf2 100644 --- a/src/packages/tabs/demos/taro/demo5.tsx +++ b/src/packages/tabs/demos/taro/demo5.tsx @@ -10,6 +10,7 @@ const Demo5 = () => { setTabvalue(value) }} activeType="button" + style={{ '--nutui-tabs-titles-item-font-family': 'PingFang SC' }} > Tab longitem Tab 2 diff --git a/src/packages/tabs/demos/taro/demo8.tsx b/src/packages/tabs/demos/taro/demo8.tsx index 580f732675..2cafbfb418 100644 --- a/src/packages/tabs/demos/taro/demo8.tsx +++ b/src/packages/tabs/demos/taro/demo8.tsx @@ -1,11 +1,16 @@ import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react-taro' +import { Tabs, pxTransform } from '@nutui/nutui-react-taro' const Demo8 = () => { const [tabvalue, setTabvalue] = useState('0') return ( { setTabvalue(value) diff --git a/src/packages/tabs/doc.en-US.md b/src/packages/tabs/doc.en-US.md index adb3fe22ae..d07f50526e 100644 --- a/src/packages/tabs/doc.en-US.md +++ b/src/packages/tabs/doc.en-US.md @@ -200,6 +200,22 @@ When autoHeight is set to true, nut-tabs and nut-tabs\_\_content will change wit ::: +### Custom Tab Bar - Auxiliary Info + +:::demo + + + +::: + +### Left Align - Auxiliary Operation + +:::demo + + + +::: + ## Tabs ### Props @@ -237,22 +253,30 @@ The component provides the following CSS variables, which can be used to customi | Name | Description | Default | | --- | --- | --- | -| \--nutui-tabs-titles-height | height of titles in horizontal direction | `44px` | -| \--nutui-tabs-titles-background-color | Tab title background color | `$color-background` | -| \--nutui-tabs-title-gap | Tab title margin | `0px` | -| \--nutui-tabs-titles-font-size | Tab title font size | `$font-size-base` | +| \--nutui-tabs-titles-height | height of titles in horizontal direction | `36px` | +| \--nutui-tabs-titles-background-color | Tab title background color | `$color-background-overlay` | +| \--nutui-tabs-titles-gap | Tab title margin | `8px` | +| \--nutui-tabs-titles-font-size | Tab title font size | `$font-size-md` | +| \--nutui-tabs-titles-item-font-family | Tab title font family | `PingFangSC-Semibold` | +| \--nutui-tabs-titles-item-font-weight | Tab title font weight | `$font-weight-bold` | | \--nutui-tabs-titles-item-min-width | Minimum width of horizontal titles | `50px` | | \--nutui-tabs-titles-item-color | Tab titles font color | `$color-title` | | \--nutui-tabs-titles-item-active-color | Tab selected titles font color | `$color-primary` | | \--nutui-tabs-titles-item-active-font-weight | Tab selected titles font weight | `$font-weight-bold` | -| \--nutui-tabs-titles-item-active-font-size | Tab selected titles font size | `$font-size-l` | +| \--nutui-tabs-titles-item-active-font-family | Tab selected titles font family | `PingFangSC-Semibold` | +| \--nutui-tabs-titles-item-active-font-size | Tab selected titles font size | `$font-size-md` | | \--nutui-tabs-titles-item-active-background-color | Background color of active tab titles in horizontal direction | `$color-background-overlay` | -| \--nutui-tabs-tab-line-width | Horizontal active tab line width | `12px` | +| \--nutui-tabs-tab-line-width | Horizontal active tab line width | `16px` | | \--nutui-tabs-tab-line-height | Height of active tabs line in horizontal direction | `2px` | | \--nutui-tabs-tab-line-color | Horizontal line color | `$color-primary` | | \--nutui-tabs-line-bottom | Horizontal line distance | `15%` | | \--nutui-tabs-line-border-radius | rounded corners for horizontal lines | `2px` | | \--nutui-tabs-tab-line-opacity | Opacity of horizontal tabs | `1` | +| \--nutui-tabs-button-border-radius | Border radius of button-type tabs | `6px` | +| \--nutui-tabs-button-font-size | Font size of unselected button-type title | `$font-size-base` | +| \--nutui-tabs-button-active-font-size | Font size of selected button-type title | `$font-size-base` | +| \--nutui-tabs-button-active-font-weight | Font weight of selected button-type title | `$font-weight-bold` | +| \--nutui-tabs-button-background-color | Background color of unselected button-type title | `var(--nutui-color-background-component)` | | \--nutui-tabs-vertical-titles-width | Width of vertical titles | `100px` | | \--nutui-tabs-vertical-titles-item-height | height of vertical titles | `40px` | | \--nutui-tabs-vertical-tab-line-color | vertical line color | `linear-gradient(180deg, $color-primary 0%, rgba(#fa2c19, 0.15) 100%)` | diff --git a/src/packages/tabs/doc.md b/src/packages/tabs/doc.md index 53448f5b56..961d6d41bb 100644 --- a/src/packages/tabs/doc.md +++ b/src/packages/tabs/doc.md @@ -200,6 +200,22 @@ import { Tabs } from '@nutui/nutui-react' ::: +### 自定义标签栏-辅助信息 + +:::demo + + + +::: + +### 左对齐-辅助操作 + +:::demo + + + +::: + ## Tabs ### Props @@ -237,22 +253,30 @@ import { Tabs } from '@nutui/nutui-react' | 名称 | 说明 | 默认值 | | --- | --- | --- | -| \--nutui-tabs-titles-height | 水平方向标题的高度 | `44px` | -| \--nutui-tabs-titles-background-color | Tab 标题的背景色 | `$color-background` | -| \--nutui-tabs-title-gap | Tab 标题的左右 margin | `0px` | -| \--nutui-tabs-titles-font-size | Tab 标题的字号 | `$font-size-base` | +| \--nutui-tabs-titles-height | 水平方向标题的高度 | `36px` | +| \--nutui-tabs-titles-background-color | Tab 标题的背景色 | `$color-background-overlay` | +| \--nutui-tabs-titles-gap | Tab 标题的左右 margin | `8px` | +| \--nutui-tabs-titles-font-size | Tab 标题的字号 | `$font-size-md` | +| \--nutui-tabs-titles-item-font-family | Tab 标题的字体 | `PingFangSC-Semibold` | +| \--nutui-tabs-titles-item-font-weight | Tab 标题的字重 | `$font-weight-bold` | | \--nutui-tabs-titles-item-min-width | 水平方向标题的最小宽度 | `50px` | | \--nutui-tabs-titles-item-color | Tab 标题的字色 | `$color-title` | | \--nutui-tabs-titles-item-active-color | Tab 选中标题的字色 | `$color-primary` | | \--nutui-tabs-titles-item-active-font-weight | Tab 选中标题的字重 | `$font-weight-bold` | -| \--nutui-tabs-titles-item-active-font-size | Tab 选中标题的字号 | `$font-size-l` | +| \--nutui-tabs-titles-item-active-font-family | Tab 选中标题的字体 | `PingFangSC-Semibold` | +| \--nutui-tabs-titles-item-active-font-size | Tab 选中标题的字号 | `$font-size-md` | | \--nutui-tabs-titles-item-active-background-color | 水平方向激活选项卡标题的背景色 | `$color-background-overlay` | -| \--nutui-tabs-tab-line-width | 水平方向激活选项卡线条的宽度 | `12px` | +| \--nutui-tabs-tab-line-width | 水平方向激活选项卡线条的宽度 | `16px` | | \--nutui-tabs-tab-line-height | 水平方向激活选项卡线条的高度 | `2px` | | \--nutui-tabs-tab-line-color | 水平方向线条颜色 | `$color-primary` | | \--nutui-tabs-line-bottom | 水平方向线条距离 | `15%` | | \--nutui-tabs-line-border-radius | 水平方向线的圆角 | `2px` | | \--nutui-tabs-tab-line-opacity | 水平方向线的透明度 | `1` | +| \--nutui-tabs-button-border-radius | 按钮模式选项卡的圆角 | `6px` | +| \--nutui-tabs-button-font-size | 按钮模式未选中标题的字号 | `$font-size-base` | +| \--nutui-tabs-button-active-font-size | 按钮模式选中标题的字号 | `$font-size-base` | +| \--nutui-tabs-button-active-font-weight | 按钮模式选中标题的字重 | `$font-weight-bold` | +| \--nutui-tabs-button-background-color | 按钮模式未选中标题的背景色 | `var(--nutui-color-background-component)` | | \--nutui-tabs-vertical-titles-width | 垂直方向标题的宽度 | `100px` | | \--nutui-tabs-vertical-titles-item-height | 垂直方向标题的高度 | `40px` | | \--nutui-tabs-vertical-tab-line-color | 垂直方向线条颜色 | `linear-gradient(180deg, $color-primary 0%, rgba(#FF0F23, 0.15) 100%)` | diff --git a/src/packages/tabs/doc.taro.md b/src/packages/tabs/doc.taro.md index 789ca19516..a81997546c 100644 --- a/src/packages/tabs/doc.taro.md +++ b/src/packages/tabs/doc.taro.md @@ -198,6 +198,22 @@ import { Tabs } from '@nutui/nutui-react-taro' ::: +### 自定义标签栏-辅助信息 + +:::demo + + + +::: + +### 左对齐-辅助操作 + +:::demo + + + +::: + ## Tabs ### Props @@ -235,22 +251,30 @@ import { Tabs } from '@nutui/nutui-react-taro' | 名称 | 说明 | 默认值 | | --- | --- | --- | -| \--nutui-tabs-titles-height | 水平方向标题的高度 | `44px` | -| \--nutui-tabs-titles-background-color | Tab 标题的背景色 | `$color-background` | -| \--nutui-tabs-title-gap | Tab 标题的左右 margin | `0px` | -| \--nutui-tabs-titles-font-size | Tab 标题的字号 | `$font-size-base` | +| \--nutui-tabs-titles-height | 水平方向标题的高度 | `36px` | +| \--nutui-tabs-titles-background-color | Tab 标题的背景色 | `$color-background-overlay` | +| \--nutui-tabs-titles-gap | Tab 标题的左右 margin | `8px` | +| \--nutui-tabs-titles-font-size | Tab 标题的字号 | `$font-size-md` | +| \--nutui-tabs-titles-item-font-family | Tab 标题的字体 | `PingFangSC-Semibold` | +| \--nutui-tabs-titles-item-font-weight | Tab 标题的字重 | `$font-weight-bold` | | \--nutui-tabs-titles-item-min-width | 水平方向标题的最小宽度 | `50px` | | \--nutui-tabs-titles-item-color | Tab 标题的字色 | `$color-title` | | \--nutui-tabs-titles-item-active-color | Tab 选中标题的字色 | `$color-primary` | | \--nutui-tabs-titles-item-active-font-weight | Tab 选中标题的字重 | `$font-weight-bold` | -| \--nutui-tabs-titles-item-active-font-size | Tab 选中标题的字号 | `$font-size-l` | +| \--nutui-tabs-titles-item-active-font-family | Tab 选中标题的字体 | `PingFangSC-Semibold` | +| \--nutui-tabs-titles-item-active-font-size | Tab 选中标题的字号 | `$font-size-md` | | \--nutui-tabs-titles-item-active-background-color | 水平方向激活选项卡标题的背景色 | `$color-background-overlay` | -| \--nutui-tabs-tab-line-width | 水平方向激活选项卡线条的宽度 | `12px` | +| \--nutui-tabs-tab-line-width | 水平方向激活选项卡线条的宽度 | `16px` | | \--nutui-tabs-tab-line-height | 水平方向激活选项卡线条的高度 | `2px` | | \--nutui-tabs-tab-line-color | 水平方向线条颜色 | `$color-primary` | | \--nutui-tabs-line-bottom | 水平方向线条距离 | `15%` | | \--nutui-tabs-line-border-radius | 水平方向线的圆角 | `2px` | | \--nutui-tabs-tab-line-opacity | 水平方向线的透明度 | `1` | +| \--nutui-tabs-button-border-radius | 按钮模式选项卡的圆角 | `6px` | +| \--nutui-tabs-button-font-size | 按钮模式未选中标题的字号 | `$font-size-base` | +| \--nutui-tabs-button-active-font-size | 按钮模式选中标题的字号 | `$font-size-base` | +| \--nutui-tabs-button-active-font-weight | 按钮模式选中标题的字重 | `$font-weight-bold` | +| \--nutui-tabs-button-background-color | 按钮模式未选中标题的背景色 | `var(--nutui-color-background-component)` | | \--nutui-tabs-vertical-titles-width | 垂直方向标题的宽度 | `100px` | | \--nutui-tabs-vertical-titles-item-height | 垂直方向标题的高度 | `40px` | | \--nutui-tabs-vertical-tab-line-color | 垂直方向线条颜色 | `linear-gradient(180deg, $color-primary 0%, rgba(#FF0F23, 0.15) 100%)` | diff --git a/src/packages/tabs/doc.zh-TW.md b/src/packages/tabs/doc.zh-TW.md index a9a0d4cd6b..88753dcf4b 100644 --- a/src/packages/tabs/doc.zh-TW.md +++ b/src/packages/tabs/doc.zh-TW.md @@ -198,6 +198,22 @@ import { Tabs } from '@nutui/nutui-react' ::: +### 自定義標簽欄-輔助信息 + +:::demo + + + +::: + +### 左對齊-輔助操作 + +:::demo + + + +::: + ## Tabs ### Props @@ -235,22 +251,30 @@ import { Tabs } from '@nutui/nutui-react' | 名稱 | 說明 | 默認值 | | --- | --- | --- | -| \--nutui-tabs-titles-height | 水平方向標題的高度 | `44px` | -| \--nutui-tabs-titles-background-color | Tab 標題的背景色 | `$color-background` | -| \--nutui-tabs-title-gap | Tab 標題的左右 margin | `0px` | -| \--nutui-tabs-titles-font-size | Tab 標題的字號 | `$font-size-base` | +| \--nutui-tabs-titles-height | 水平方向標題的高度 | `36px` | +| \--nutui-tabs-titles-background-color | Tab 標題的背景色 | `$color-background-overlay` | +| \--nutui-tabs-titles-gap | Tab 標題的左右 margin | `8px` | +| \--nutui-tabs-titles-font-size | Tab 標題的字號 | `$font-size-md` | +| \--nutui-tabs-titles-item-font-family | Tab 標題的字體 | `PingFangSC-Semibold` | +| \--nutui-tabs-titles-item-font-weight | Tab 標題的字重 | `$font-weight-bold` | | \--nutui-tabs-titles-item-min-width | 水平方向標題的最小寬度 | `50px` | | \--nutui-tabs-titles-item-color | Tab 標題的字色 | `$color-title` | | \--nutui-tabs-titles-item-active-color | Tab 選中標題的字色 | `$color-primary` | | \--nutui-tabs-titles-item-active-font-weight | Tab 選中標題的字重 | `$font-weight-bold` | -| \--nutui-tabs-titles-item-active-font-size | Tab 選中標題的字號 | `$font-size-l` | +| \--nutui-tabs-titles-item-active-font-family | Tab 選中標題的字體 | `PingFangSC-Semibold` | +| \--nutui-tabs-titles-item-active-font-size | Tab 選中標題的字號 | `$font-size-md` | | \--nutui-tabs-titles-item-active-background-color | 水平方向激活選項卡標題的背景色 | `$color-background-overlay` | -| \--nutui-tabs-tab-line-width | 水平方向激活選項卡線條的寬度 | `12px` | +| \--nutui-tabs-tab-line-width | 水平方向激活選項卡線條的寬度 | `16px` | | \--nutui-tabs-tab-line-height | 水平方向激活選項卡線條的高度 | `2px` | | \--nutui-tabs-tab-line-color | 水平方向線條顏色 | `$color-primary` | | \--nutui-tabs-line-bottom | 水平方向線條距離 | `15%` | | \--nutui-tabs-line-border-radius | 水平方向線的圓角 | `2px` | | \--nutui-tabs-tab-line-opacity | 水平方向線的透明度 | `1` | +| \--nutui-tabs-button-border-radius | 按鈕模式選項卡的圓角 | `6px` | +| \--nutui-tabs-button-font-size | 按鈕模式未選中標題的字號 | `$font-size-base` | +| \--nutui-tabs-button-active-font-size | 按鈕模式選中標題的字號 | `$font-size-base` | +| \--nutui-tabs-button-active-font-weight | 按鈕模式選中標題的字重 | `$font-weight-bold` | +| \--nutui-tabs-button-background-color | 按鈕模式未選中標題的背景色 | `var(--nutui-color-background-component)` | | \--nutui-tabs-vertical-titles-width | 垂直方向標題的寬度 | `100px` | | \--nutui-tabs-vertical-titles-item-height | 垂直方向標題的高度 | `40px` | | \--nutui-tabs-vertical-tab-line-color | 垂直方向線條顏色 | `linear-gradient(180deg, $color-primary 0%, rgba(#FF0F23, 0.15) 100%)` | diff --git a/src/packages/tabs/icon.taro.tsx b/src/packages/tabs/icon.taro.tsx new file mode 100644 index 0000000000..a3e6cd437b --- /dev/null +++ b/src/packages/tabs/icon.taro.tsx @@ -0,0 +1,28 @@ +import React, { FC, CSSProperties } from 'react' +import { View } from '@tarojs/components' + +const shoulderColor = + 'var(--nutui-tabs-titles-item-active-background-color, var(--nutui-color-background-overlay, #ffffff))' + +// 卡片肩部只保留这一份图形(右肩形状);左肩在样式层水平镜像复用,不再单独维护第二份资源 +const shoulder = + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSI0MCIgdmlld0JveD0iMCAwIDI0IDQwIiBmaWxsPSJub25lIj48cGF0aCBkPSJNMCAwTDEuNTIxMzY5MyAwQzUuMjk4Nzk2NyAwIDguNTYxNDY5MSAyLjY0MjIwNjIgOS4zNDY2NDA2IDYuMzM3MTMwMUwxNS4xNTMzNiAzMy42NjI4NjhDMTUuOTM4NTMxIDM3LjM1Nzc5MiAxOS4yMDEyMDIgNDAgMjIuOTc4NjMgNDBMMjQgNDBMMjIuOTggNDBMMCA0MEwwIDBaIiBmaWxsPSIjZmZmZmZmIi8+PC9zdmc+' + +export interface CardShoulderProps { + className?: string +} + +export const CardShoulder: FC = ({ className }) => { + const mask = `url('${shoulder}') 0 0/100% 100% no-repeat` + const maskStyle = ( + process.env.TARO_ENV === 'h5' + ? { mask, WebkitMask: mask } + : { mask, '-webkitMask': mask } + ) as CSSProperties + return ( + + ) +} diff --git a/src/packages/tabs/icon.tsx b/src/packages/tabs/icon.tsx new file mode 100644 index 0000000000..ed95eaf8e3 --- /dev/null +++ b/src/packages/tabs/icon.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react' + +const shoulderColor = + 'var(--nutui-tabs-titles-item-active-background-color, var(--nutui-color-background-overlay, #ffffff))' + +// 卡片肩部只保留这一份图形(右肩形状);左肩在样式层水平镜像复用,不再单独维护第二份资源 +const SHOULDER_PATH = + 'M0 0L1.5213693 0C5.2987967 0 8.5614691 2.6422062 9.3466406 6.3371301L15.15336 33.662868C15.938531 37.357792 19.201202 40 22.97863 40L24 40L22.98 40L0 40L0 0Z' + +export interface CardShoulderProps { + className?: string +} + +export const CardShoulder: FC = ({ className }) => { + return ( + + + + ) +} diff --git a/src/packages/tabs/tabs.scss b/src/packages/tabs/tabs.scss index 56f86215a3..b3e5c588ad 100644 --- a/src/packages/tabs/tabs.scss +++ b/src/packages/tabs/tabs.scss @@ -34,14 +34,18 @@ &-left { justify-content: flex-start; .nut-tabs-titles-item { - padding: 0 scale-px(22px); + padding: 0 scale-px(8px); } } &-right { justify-content: flex-end; .nut-tabs-titles-item { - padding: 0 scale-px(22px); + padding: 0 scale-px(8px); + } + + .nut-tabs-list { + justify-content: flex-end; } } @@ -56,6 +60,8 @@ line-height: $tabs-titles-height; min-width: $tabs-titles-item-min-width; font-size: $tabs-titles-font-size; + font-family: $tabs-titles-item-font-family; + font-weight: $tabs-titles-item-font-weight; color: $tabs-titles-item-color; text-overflow: ellipsis; white-space: nowrap; @@ -71,6 +77,7 @@ &-text { color: $tabs-titles-item-color; + font-family: $tabs-titles-item-font-family; } &-smile, @@ -100,8 +107,8 @@ } &-smile-icon { - width: scale-px(40px); - height: scale-px(20px); + width: scale-px(12px); + height: scale-px(12px); } &-active { @@ -111,6 +118,8 @@ .nut-tabs-titles-item-text { color: $tabs-titles-item-active-color; + font-family: $tabs-titles-item-active-font-family; + font-size: $tabs-titles-item-active-font-size; font-weight: $tabs-titles-item-active-font-weight; } @@ -128,8 +137,8 @@ /* #ifndef dynamic*/ overflow: unset; /* #endif */ - width: scale-px(40px); - height: scale-px(20px); + width: scale-px(12px); + height: scale-px(12px); .nut-icon { color: $tabs-titles-item-active-color; @@ -153,7 +162,7 @@ &-active { .nut-tabs-titles-item-text, .nut-icon { - color: $color-title; + color: $tabs-titles-item-active-color; font-size: $tabs-titles-item-active-font-size; } } @@ -162,17 +171,63 @@ &-card { .nut-tabs-titles-item { + padding: 0 scale-px(8px); + flex-direction: column; + justify-content: center; + line-height: initial; + background: transparent; + &-active { + position: relative; + z-index: 1; + margin: 0 scale-px(24px); font-weight: $font-weight-bold; - background-color: $white; - border-radius: $radius-base $radius-base 0 0; + background-color: $tabs-titles-item-active-background-color; + + .nut-tabs-titles-item-shoulder-left, + .nut-tabs-titles-item-shoulder-right { + position: absolute; + bottom: 0; + width: scale-px(24px); + height: 100%; + display: block; + pointer-events: none; + } + + .nut-tabs-titles-item-shoulder-left { + left: scale-px(-24px); + // 左肩复用右肩图形,水平镜像即可,无需第二份资源 + transform: scaleX(-1); + } + + .nut-tabs-titles-item-shoulder-right { + right: scale-px(-24px); + } + + // 贴边端由 TSX 按可滑动性、对齐方式与书写方向(RTL)统一计算, + // 样式层只认这两个修饰类:贴边侧取消留白并回退成直角(隐藏对应肩部) + &.nut-tabs-titles-item-active--flush-left { + margin-left: 0; + + .nut-tabs-titles-item-shoulder-left { + display: none; + } + } + + &.nut-tabs-titles-item-active--flush-right { + margin-right: 0; + + .nut-tabs-titles-item-shoulder-right { + display: none; + } + } } } } &-button { .nut-tabs-titles-item { - padding: 0 scale-px(10px); + padding: 0 scale-px(4px); .nut-tabs-titles-item-text { flex: 1; height: scale-px(28px); @@ -180,16 +235,20 @@ align-items: center; justify-content: center; padding: 0 scale-px(8px); + background: $tabs-tab-button-background-color; + border-radius: $tabs-tab-button-border-radius; + font-size: $tabs-tab-button-font-size; + font-weight: $font-weight; + color: $color-text; } } .nut-tabs-titles-item-active { .nut-tabs-titles-item-text { - background: $color-default-light; color: $tabs-titles-item-active-color; - border-radius: $tabs-tab-button-border-radius; - font-weight: $font-weight-bold; + font-size: $tabs-tab-button-active-font-size; + font-weight: $tabs-tab-button-active-font-weight; background-color: $tabs-tab-button-active-background-color; - border: $tabs-tab-button-active-border; + box-shadow: $tabs-tab-button-active-border; } } } @@ -220,6 +279,29 @@ } } +// 标题栏边缘留白:该端 item 外侧 padding 翻倍,使边缘留白 = 相邻 tab 间距 +// 均分补首尾两端,左对齐仅补左端,右对齐仅补右端;水平方向生效 +.nut-tabs-horizontal { + .nut-tabs-titles:not(.nut-tabs-titles-right) + .nut-tabs-titles-item:first-child { + padding-left: calc(#{$tabs-titles-gap} * 2); + } + + .nut-tabs-titles:not(.nut-tabs-titles-left) .nut-tabs-titles-item:last-child { + padding-right: calc(#{$tabs-titles-gap} * 2); + } + + .nut-tabs-titles-button { + &:not(.nut-tabs-titles-right) .nut-tabs-titles-item:first-child { + padding-left: 8px; + } + + &:not(.nut-tabs-titles-left) .nut-tabs-titles-item:last-child { + padding-right: 8px; + } + } +} + .nut-tabs-vertical { .nut-tabs-ellipsis { white-space: break-spaces; @@ -265,15 +347,15 @@ &-active { background-color: $tabs-titles-item-active-background-color; .nut-tabs-titles-item-line { - left: scale-px(10px); + left: scale-px(8px); width: $tabs-vertical-tab-line-width; height: $tabs-vertical-tab-line-height; background: $tabs-vertical-tab-line-color; } .nut-tabs-titles-item-smile { - right: scale-px(-12px); - bottom: -2%; + right: scale-px(3px); + bottom: 12%; left: auto; transform: rotate(320deg); } @@ -311,7 +393,7 @@ .nut-tabs-titles-item-smile { left: 50%; right: auto; - bottom: scale-px(-3px); + bottom: 4%; transform: translate(-50%, 0) rotate(0deg); } } @@ -360,6 +442,33 @@ } } +// RTL 边缘留白镜像:视觉左端=物理右侧、视觉右端=物理左侧,翻转补端方向 +[dir='rtl'] .nut-tabs-horizontal, +.nut-rtl .nut-tabs-horizontal { + .nut-tabs-titles:not(.nut-tabs-titles-right) + .nut-tabs-titles-item:first-child { + padding-left: $tabs-titles-gap; + padding-right: calc(#{$tabs-titles-gap} * 2); + } + + .nut-tabs-titles:not(.nut-tabs-titles-left) .nut-tabs-titles-item:last-child { + padding-right: $tabs-titles-gap; + padding-left: calc(#{$tabs-titles-gap} * 2); + } + + .nut-tabs-titles-button { + &:not(.nut-tabs-titles-right) .nut-tabs-titles-item:first-child { + padding-left: 4px; + padding-right: 8px; + } + + &:not(.nut-tabs-titles-left) .nut-tabs-titles-item:last-child { + padding-right: 4px; + padding-left: 8px; + } + } +} + [dir='rtl'] .nut-tabs-vertical, .nut-rtl .nut-tabs-vertical { .nut-tabs-titles { @@ -379,7 +488,7 @@ } .nut-tabs-titles-item-smile { - left: scale-px(-12px); + left: scale-px(3px); right: auto; transform: rotate(-320deg); } diff --git a/src/packages/tabs/tabs.taro.tsx b/src/packages/tabs/tabs.taro.tsx index 296991282a..0573c73ac4 100644 --- a/src/packages/tabs/tabs.taro.tsx +++ b/src/packages/tabs/tabs.taro.tsx @@ -1,4 +1,10 @@ -import React, { FunctionComponent, useEffect, useRef, useState } from 'react' +import React, { + FunctionComponent, + useCallback, + useEffect, + useRef, + useState, +} from 'react' import { ScrollView, View } from '@tarojs/components' import classNames from 'classnames' import { JoySmile } from '@nutui/icons-react-taro' @@ -10,8 +16,11 @@ import { useForceUpdate } from '@/hooks/use-force-update' import raf from '@/utils/raf' import { useUuid } from '@/hooks/use-uuid' import { useRtl } from '../configprovider/configprovider.taro' +import { CardShoulder } from './icon.taro' +import { getCardFlushSides } from './utils' import { TabsTitle, TaroTabsProps } from '@/types' import { td } from '@/utils/taro/platform' +import { calcByProfile } from '@/utils' const defaultProps = { ...ComponentDefaults, @@ -24,6 +33,14 @@ const defaultProps = { } as TaroTabsProps const classPrefix = 'nut-tabs' + +// 卡片模式下激活卡片两侧共预留 margin: 0 scale-px(24px),而 boundingClientRect 不含 margin, +// 估算可滑动性与滚动钳制时都需按最大 48px(2 * 24px)回补,统一在此维护; +// 设计基准值经 calcByProfile 换算,与 SCSS 的 scale-px 共用同一缩放系数 --nut-scale-f +const CARD_ACTIVE_MARGIN_TOTAL = 48 + +const getCardActiveMarginTotal = () => calcByProfile(CARD_ACTIVE_MARGIN_TOTAL) + export const Tabs: FunctionComponent> & { TabPane: typeof TabPane } = (props) => { @@ -56,7 +73,9 @@ export const Tabs: FunctionComponent> & { onChange, }) - const titleItemsRef = useRef([]) + // 卡片模式下,标题栏可横向滑动时激活卡片贴合两端边缘;不可滑动时(全部 tab 放得下) + // 贴边端直角、留白端恢复双肩 + const [isScrollable, setIsScrollable] = useState(!align) const getTitles = () => { const titles: TabsTitle[] = [] @@ -102,7 +121,7 @@ export const Tabs: FunctionComponent> & { [`${classPrefix}-titles-${align}`]: align, }) - const getRect = (selector: string) => { + const getRect = useCallback((selector: string) => { return new Promise((resolve) => { const query = createSelectorQuery() const scope = tabsRef.current?._scope @@ -114,8 +133,8 @@ export const Tabs: FunctionComponent> & { resolve(rect[0]) }) }) - } - const getAllRect = (selector: string) => { + }, []) + const getAllRect = useCallback((selector: string) => { return new Promise((resolve) => { const query = createSelectorQuery() const scope = tabsRef.current?._scope @@ -127,7 +146,7 @@ export const Tabs: FunctionComponent> & { resolve(rect[0]) }) }) - } + }, []) type RectItem = { bottom: number dataset: { sid: string } @@ -138,6 +157,30 @@ export const Tabs: FunctionComponent> & { top: number width: number } + const measureScrollable = useCallback(() => { + if (activeType !== 'card' || direction !== 'horizontal' || !align) return + raf(() => { + Promise.all([ + getRect(`#nut-tabs-titles-${name || uuid}`), + getAllRect(`#nut-tabs-titles-${name || uuid} .nut-tabs-titles-item`), + ]).then(([navRect, itemRects]: any) => { + if (!navRect || !itemRects || !itemRects.length) return + const totalWidth = itemRects.reduce( + (sum: number, curr: RectItem) => sum + curr.width, + 0 + ) + // 卡片模式 active 卡片 margin 不在 boundingClientRect 内,按 CARD_ACTIVE_MARGIN_TOTAL 预留, + // 与 scrollIntoView 的钳制口径一致,避免临界内容误判为放得下 + setIsScrollable( + totalWidth + getCardActiveMarginTotal() - navRect.width > 1 + ) + }) + }) + }, [activeType, direction, name, uuid, getRect, getAllRect, align]) + // 标题/内容变化后重新布局再测;挂载后首帧由默认 false 呈现对称样式,测量完异步翻转 + useEffect(() => { + measureScrollable() + }, [measureScrollable, children]) const scrollWithAnimation = useRef(false) const [scrollLeft, setScrollLeft] = useState(0) const [scrollTop, setScrollTop] = useState(0) @@ -180,9 +223,14 @@ export const Tabs: FunctionComponent> & { const left = titleRects .slice(0, index) .reduce((prev: number, curr: RectItem) => prev + curr.width, 0) + // 卡片模式 active 卡片带 margin: 0 24px(贴边侧为 0),boundingClientRect 不含 margin, + // 矩形宽求和会少算,钳制点偏短导致滚动到末尾时末项卡片显示不全; + // 按 CARD_ACTIVE_MARGIN_TOTAL 预留,超设部分由 ScrollView 自动截断到真实最大滚动距离 + const marginExtra = + activeType === 'card' ? getCardActiveMarginTotal() : 0 to = Math.min( Math.max(0, left - (navRect.width - titleRect.width) / 2), - totalWidth - navRect.width + totalWidth + marginExtra - navRect.width ) } @@ -256,21 +304,43 @@ export const Tabs: FunctionComponent> & { {!!title && typeof title === 'function' ? title() : titles.current.map((item, index) => { + const isActive = + !item.disabled && String(item.value) === String(value) + const flush = + isActive && + activeType === 'card' && + direction === 'horizontal' + ? getCardFlushSides(index, titles.current.length, { + scrollable: isScrollable, + rtl, + align, + }) + : { left: false, right: false } return ( - titleItemsRef.current.push(ref) - } id={`scrollIntoView${index}`} onClick={() => tabChange(item)} className={classNames(`${classPrefix}-titles-item`, { - [`nut-tabs-titles-item-active`]: - !item.disabled && String(item.value) === String(value), + [`nut-tabs-titles-item-active`]: isActive, [`nut-tabs-titles-item-disabled`]: item.disabled, [`nut-tabs-titles-item-${align}`]: align, + [`${classPrefix}-titles-item-active--flush-left`]: + flush.left, + [`${classPrefix}-titles-item-active--flush-right`]: + flush.right, })} > + {activeType === 'card' && isActive && ( + <> + + + + )} {activeType === 'line' && ( > & { onChange, }) - const titleItemsRef = useRef([]) const navRef = useRef(null) + // 卡片模式下,标题栏可横向滑动时激活卡片贴合两端边缘;不可滑动时(全部 tab 放得下) + // 贴边端直角、留白端恢复双肩,依赖该标记切换样式 + const [isScrollable, setIsScrollable] = useState(!align) + const measureScrollable = useCallback(() => { + if (activeType !== 'card' || direction !== 'horizontal' || !align) return + const nav = navRef.current + if (!nav) return + setIsScrollable(nav.scrollWidth - nav.clientWidth > 1) + }, [activeType, direction, align]) + + // 挂载后立即测量,避免首帧样式闪变 + useLayoutEffect(() => { + measureScrollable() + }, [measureScrollable]) + + // 标题/内容变化后重新布局,再测一次;窗口尺寸变化时也重测 + useEffect(() => { + raf(measureScrollable) + }, [children, measureScrollable]) + useEffect(() => { + // 仅卡片模式需要按可用宽度重测;其余模式提前返回,避免挂无用的 resize 监听 + if (activeType !== 'card' || direction !== 'horizontal' || !align) return + const onResize = () => raf(measureScrollable) + window.addEventListener('resize', onResize) + return () => window.removeEventListener('resize', onResize) + }, [activeType, direction, align, measureScrollable]) + const scrollDirection = ( nav: HTMLDivElement, to: number, @@ -71,13 +106,16 @@ export const Tabs: FunctionComponent> & { const scrollIntoView = (index: number, immediate?: boolean) => { const nav = navRef.current - const titleItem = titleItemsRef.current - const titlesLength = titles.current.length - const itemLength = titleItem.length - if (!nav || !titleItem || !titleItem[itemLength - titlesLength + index]) { + if (!nav) { + return + } + const titleItems = nav.querySelectorAll( + `.${classPrefix}-titles-item` + ) + const title = titleItems[index] + if (!title) { return } - const title = titleItem[itemLength - titlesLength + index] let to = 0 if (direction === 'vertical') { const runTop = title.offsetTop - nav.offsetTop + 10 @@ -128,7 +166,6 @@ export const Tabs: FunctionComponent> & { ) const classesTitle = classNames(`${classPrefix}-titles`, { [`${classPrefix}-titles-${activeType}`]: activeType, - [`${classPrefix}-titles-scrollable`]: true, [`${classPrefix}-titles-${align}`]: align, }) @@ -162,19 +199,41 @@ export const Tabs: FunctionComponent> & {
{!!title && typeof title === 'function' ? title() - : titles.current.map((item) => { + : titles.current.map((item, index) => { + const isActive = + !item.disabled && String(item.value) === String(value) + const flush = + isActive && activeType === 'card' && direction === 'horizontal' + ? getCardFlushSides(index, titles.current.length, { + scrollable: isScrollable, + rtl, + align, + }) + : { left: false, right: false } return (
titleItemsRef.current.push(ref)} onClick={() => tabChange(item)} className={classNames(`${classPrefix}-titles-item`, { - [`nut-tabs-titles-item-active`]: - !item.disabled && String(item.value) === String(value), + [`nut-tabs-titles-item-active`]: isActive, [`nut-tabs-titles-item-disabled`]: item.disabled, [`nut-tabs-titles-item-${align}`]: align, + [`${classPrefix}-titles-item-active--flush-left`]: + flush.left, + [`${classPrefix}-titles-item-active--flush-right`]: + flush.right, })} > + {activeType === 'card' && isActive && ( + <> + + + + )} {activeType === 'line' && (
{ + if (count <= 0 || index < 0 || index >= count) return NO_FLUSH + + // 起始边/结束边:书写方向决定其在物理方向上的映射(RTL 呈镜像) + const startSide: PositionX = options.rtl ? 'right' : 'left' + const endSide: PositionX = options.rtl ? 'left' : 'right' + const isFirst = index === 0 + const isLast = index === count - 1 + + // 内容可滑动时,DOM 首/末项分别贴住起始/结束边 + if (options.scrollable) { + return { + left: (isFirst && startSide === 'left') || (isLast && endSide === 'left'), + right: + (isFirst && startSide === 'right') || (isLast && endSide === 'right'), + } + } + + // 不可滑动时(仅在设置了 align 时出现),内容整体靠向 align 指定的一侧, + // 只有贴靠该侧的那一项才是贴边项 + const packedSide = options.align === 'right' ? endSide : startSide + const packedItem = packedSide === startSide ? isFirst : isLast + + return { + left: packedItem && packedSide === 'left', + right: packedItem && packedSide === 'right', + } +} diff --git a/src/styles/variables-daojia.scss b/src/styles/variables-daojia.scss index f8aed1b0af..7b339d695b 100644 --- a/src/styles/variables-daojia.scss +++ b/src/styles/variables-daojia.scss @@ -1381,9 +1381,13 @@ $pagination-progress-inactive-color: var( $tabs-titles-height: var(--nutui-tabs-titles-height, 44px) !default; $tabs-titles-background-color: var(--nutui-tabs-titles-background-color, $color-background) !default; -$tabs-titles-gap: var(--nutui-tabs-titles-gap, 12px) !default; +$tabs-titles-gap: var(--nutui-tabs-titles-gap, 8px) !default; $tabs-titles-font-size: var(--nutui-tabs-titles-font-size, - $font-size-base) !default; + $font-size-md) !default; +$tabs-titles-item-font-family: var(--nutui-tabs-titles-item-font-family, + PingFangSC-Semibold) !default; +$tabs-titles-item-font-weight: var(--nutui-tabs-titles-item-font-weight, + $font-weight-bold) !default; $tabs-titles-item-min-width: var(--nutui-tabs-titles-item-min-width, 50px) !default; $tabs-titles-item-color: var(--nutui-tabs-titles-item-color, @@ -1392,8 +1396,10 @@ $tabs-titles-item-active-color: var(--nutui-tabs-titles-item-active-color, $color-primary) !default; $tabs-titles-item-active-font-weight: var(--nutui-tabs-titles-item-active-font-weight, $font-weight-bold) !default; +$tabs-titles-item-active-font-family: var(--nutui-tabs-titles-item-active-font-family, + PingFangSC-Semibold) !default; $tabs-titles-item-active-font-size: var(--nutui-tabs-titles-item-active-font-size, - $font-size-l) !default; + $font-size-md) !default; $tabs-titles-item-active-background-color: var(--nutui-tabs-titles-item-active-background-color, $color-background-overlay) !default; $tabs-tab-line-width: var(--nutui-tabs-tab-line-width, 12px) !default; @@ -1405,10 +1411,18 @@ $tabs-tab-line-border-radius: var(--nutui-tabs-line-border-radius, $tabs-tab-line-opacity: var(--nutui-tabs-tab-line-opacity, 1) !default; $tabs-tab-button-border-radius: var(--nutui-tabs-button-border-radius, $radius-circle) !default; +$tabs-tab-button-font-size: var(--nutui-tabs-button-font-size, + $font-size-base) !default; +$tabs-tab-button-active-font-size: var(--nutui-tabs-button-active-font-size, + $font-size-base) !default; +$tabs-tab-button-active-font-weight: var(--nutui-tabs-button-active-font-weight, + $font-weight-bold) !default; +$tabs-tab-button-background-color: var(--nutui-tabs-button-background-color, + var(--nutui-color-background-component)) !default; $tabs-tab-button-active-background-color: var(--nutui-tabs-button-active-background-color, $color-primary-light-pressed) !default; $tabs-tab-button-active-border: var(--nutui-tabs-button-active-border, - 1px solid $color-primary) !default; + inset 0 0 0 1px $color-primary) !default; $tabs-vertical-titles-width: var(--nutui-tabs-vertical-titles-width, 100px) !default; $tabs-vertical-titles-item-height: var(--nutui-tabs-vertical-titles-item-height, @@ -1422,7 +1436,7 @@ $tabs-vertical-tab-line-width: var(--nutui-tabs-vertical-tab-line-width, $tabs-vertical-tab-line-height: var(--nutui-tabs-vertical-tab-line-height, 12px) !default; $tabs-titles-item-smile-bottom: var(--nutui-tabs-titles-item-smile-bottom, - -10%) !default; + 4%) !default; $tabs-tabpane-padding: var(--nutui-tabs-tabpane-padding, 24px 20px) !default; $tabs-tabpane-background-color: var(--nutui-tabs-tabpane-background-color, #fff) !default; diff --git a/src/styles/variables-jmapp.scss b/src/styles/variables-jmapp.scss index a5490e6f8d..a1f79f54fc 100644 --- a/src/styles/variables-jmapp.scss +++ b/src/styles/variables-jmapp.scss @@ -2240,10 +2240,18 @@ $tabs-titles-background-color: var( --nutui-tabs-titles-background-color, $white ) !default; -$tabs-titles-gap: var(--nutui-tabs-titles-gap, 12px) !default; +$tabs-titles-gap: var(--nutui-tabs-titles-gap, 8px) !default; $tabs-titles-font-size: var( --nutui-tabs-titles-font-size, - $font-size-base + $font-size-md +) !default; +$tabs-titles-item-font-family: var( + --nutui-tabs-titles-item-font-family, + PingFangSC-Semibold +) !default; +$tabs-titles-item-font-weight: var( + --nutui-tabs-titles-item-font-weight, + $font-weight-bold ) !default; $tabs-titles-item-min-width: var( --nutui-tabs-titles-item-min-width, @@ -2261,9 +2269,13 @@ $tabs-titles-item-active-font-weight: var( --nutui-tabs-titles-item-active-font-weight, $font-weight-bold ) !default; +$tabs-titles-item-active-font-family: var( + --nutui-tabs-titles-item-active-font-family, + PingFangSC-Semibold +) !default; $tabs-titles-item-active-font-size: var( --nutui-tabs-titles-item-active-font-size, - $font-size-l + $font-size-md ) !default; $tabs-titles-item-active-background-color: var( --nutui-tabs-titles-item-active-background-color, @@ -2282,13 +2294,29 @@ $tabs-tab-button-border-radius: var( --nutui-tabs-button-border-radius, $radius-xs ) !default; +$tabs-tab-button-font-size: var( + --nutui-tabs-button-font-size, + $font-size-base +) !default; +$tabs-tab-button-active-font-size: var( + --nutui-tabs-button-active-font-size, + $font-size-base +) !default; +$tabs-tab-button-active-font-weight: var( + --nutui-tabs-button-active-font-weight, + $font-weight-bold +) !default; +$tabs-tab-button-background-color: var( + --nutui-tabs-button-background-color, + var(--nutui-color-background-component) +) !default; $tabs-tab-button-active-background-color: var( --nutui-tabs-button-active-background-color, $color-primary-light-pressed ) !default; $tabs-tab-button-active-border: var( --nutui-tabs-button-active-border, - 1px solid $color-primary + inset 0 0 0 1px $color-primary ) !default; $tabs-vertical-titles-width: var( --nutui-tabs-vertical-titles-width, @@ -2316,7 +2344,7 @@ $tabs-vertical-tab-line-height: var( ) !default; $tabs-titles-item-smile-bottom: var( --nutui-tabs-titles-item-smile-bottom, - -10px + 4% ) !default; $tabs-tabpane-padding: var(--nutui-tabs-tabpane-padding, 24px 20px) !default; $tabs-tabpane-background-color: var( diff --git a/src/styles/variables-jrkf.scss b/src/styles/variables-jrkf.scss index 230b1d000f..8a70775be0 100644 --- a/src/styles/variables-jrkf.scss +++ b/src/styles/variables-jrkf.scss @@ -2347,10 +2347,18 @@ $tabs-titles-background-color: var( --nutui-tabs-titles-background-color, $white ) !default; -$tabs-titles-gap: var(--nutui-tabs-titles-gap, 12px) !default; +$tabs-titles-gap: var(--nutui-tabs-titles-gap, 8px) !default; $tabs-titles-font-size: var( --nutui-tabs-titles-font-size, - $font-size-base + $font-size-md +) !default; +$tabs-titles-item-font-family: var( + --nutui-tabs-titles-item-font-family, + PingFangSC-Semibold +) !default; +$tabs-titles-item-font-weight: var( + --nutui-tabs-titles-item-font-weight, + $font-weight-bold ) !default; $tabs-titles-item-min-width: var( --nutui-tabs-titles-item-min-width, @@ -2368,9 +2376,13 @@ $tabs-titles-item-active-font-weight: var( --nutui-tabs-titles-item-active-font-weight, $font-weight-bold ) !default; +$tabs-titles-item-active-font-family: var( + --nutui-tabs-titles-item-active-font-family, + PingFangSC-Semibold +) !default; $tabs-titles-item-active-font-size: var( --nutui-tabs-titles-item-active-font-size, - $font-size-l + $font-size-md ) !default; $tabs-titles-item-active-background-color: var( --nutui-tabs-titles-item-active-background-color, @@ -2389,13 +2401,29 @@ $tabs-tab-button-border-radius: var( --nutui-tabs-button-border-radius, $radius-xs ) !default; +$tabs-tab-button-font-size: var( + --nutui-tabs-button-font-size, + $font-size-base +) !default; +$tabs-tab-button-active-font-size: var( + --nutui-tabs-button-active-font-size, + $font-size-base +) !default; +$tabs-tab-button-active-font-weight: var( + --nutui-tabs-button-active-font-weight, + $font-weight-bold +) !default; +$tabs-tab-button-background-color: var( + --nutui-tabs-button-background-color, + var(--nutui-color-background-component) +) !default; $tabs-tab-button-active-background-color: var( --nutui-tabs-button-active-background-color, $color-primary-light ) !default; $tabs-tab-button-active-border: var( --nutui-tabs-button-active-border, - 1px solid $color-primary + inset 0 0 0 1px $color-primary ) !default; $tabs-vertical-titles-width: var( --nutui-tabs-vertical-titles-width, @@ -2419,7 +2447,7 @@ $tabs-vertical-tab-line-height: var( ) !default; $tabs-titles-item-smile-bottom: var( --nutui-tabs-titles-item-smile-bottom, - -10px + 4% ) !default; $tabs-tabpane-padding: var(--nutui-tabs-tabpane-padding, 24px 20px) !default; $tabs-tabpane-background-color: var( diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 8aba0bd585..25c09c2134 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -2249,15 +2249,23 @@ $pagination-progress-inactive-color: var( ) !default; // tabs(✅) -$tabs-titles-height: var(--nutui-tabs-titles-height, scale-px(44px)) !default; +$tabs-titles-height: var(--nutui-tabs-titles-height, scale-px(36px)) !default; $tabs-titles-background-color: var( --nutui-tabs-titles-background-color, - $color-background + $color-background-overlay ) !default; -$tabs-titles-gap: var(--nutui-tabs-titles-gap, scale-px(12px)) !default; +$tabs-titles-gap: var(--nutui-tabs-titles-gap, scale-px(8px)) !default; $tabs-titles-font-size: var( --nutui-tabs-titles-font-size, - $font-size-base + $font-size-md +) !default; +$tabs-titles-item-font-family: var( + --nutui-tabs-titles-item-font-family, + PingFangSC-Semibold +) !default; +$tabs-titles-item-font-weight: var( + --nutui-tabs-titles-item-font-weight, + $font-weight-bold ) !default; $tabs-titles-item-min-width: var( --nutui-tabs-titles-item-min-width, @@ -2275,15 +2283,19 @@ $tabs-titles-item-active-font-weight: var( --nutui-tabs-titles-item-active-font-weight, $font-weight-bold ) !default; +$tabs-titles-item-active-font-family: var( + --nutui-tabs-titles-item-active-font-family, + PingFangSC-Semibold +) !default; $tabs-titles-item-active-font-size: var( --nutui-tabs-titles-item-active-font-size, - $font-size-l + $font-size-md ) !default; $tabs-titles-item-active-background-color: var( --nutui-tabs-titles-item-active-background-color, $color-background-overlay ) !default; -$tabs-tab-line-width: var(--nutui-tabs-tab-line-width, scale-px(12px)) !default; +$tabs-tab-line-width: var(--nutui-tabs-tab-line-width, scale-px(16px)) !default; $tabs-tab-line-height: var(--nutui-tabs-tab-line-height, scale-px(2px)) !default; $tabs-tab-line-color: var(--nutui-tabs-tab-line-color, $color-primary) !default; $tabs-tab-line-bottom: var(--nutui-tabs-line-bottom, 15%) !default; @@ -2294,7 +2306,23 @@ $tabs-tab-line-border-radius: var( $tabs-tab-line-opacity: var(--nutui-tabs-tab-line-opacity, 1) !default; $tabs-tab-button-border-radius: var( --nutui-tabs-button-border-radius, - $radius-circle + scale-px(6px) +) !default; +$tabs-tab-button-font-size: var( + --nutui-tabs-button-font-size, + $font-size-base +) !default; +$tabs-tab-button-active-font-size: var( + --nutui-tabs-button-active-font-size, + $font-size-base +) !default; +$tabs-tab-button-active-font-weight: var( + --nutui-tabs-button-active-font-weight, + $font-weight-bold +) !default; +$tabs-tab-button-background-color: var( + --nutui-tabs-button-background-color, + var(--nutui-color-background-component) ) !default; $tabs-tab-button-active-background-color: var( --nutui-tabs-button-active-background-color, @@ -2302,7 +2330,7 @@ $tabs-tab-button-active-background-color: var( ) !default; $tabs-tab-button-active-border: var( --nutui-tabs-button-active-border, - scale-px(1px) solid $color-primary + inset 0 0 0 scale-px(1px) $color-primary ) !default; $tabs-vertical-titles-width: var( --nutui-tabs-vertical-titles-width, @@ -2330,7 +2358,7 @@ $tabs-vertical-tab-line-height: var( ) !default; $tabs-titles-item-smile-bottom: var( --nutui-tabs-titles-item-smile-bottom, - -10% + 4% ) !default; $tabs-tabpane-padding: var(--nutui-tabs-tabpane-padding, scale-px(24px) scale-px(20px)) !default; $tabs-tabpane-background-color: var( diff --git a/src/types/spec/dialog/base.ts b/src/types/spec/dialog/base.ts index 17e00c8d37..64500fb2de 100644 --- a/src/types/spec/dialog/base.ts +++ b/src/types/spec/dialog/base.ts @@ -21,8 +21,7 @@ export interface BaseContentProps extends BasicComponent { footerDirection: DialogFooterDirection } export interface DialogWrapProps - extends WebOverlayProps, - Omit { + extends WebOverlayProps, Omit { visible: boolean overlay: boolean overlayStyle: CSSProperties @@ -56,8 +55,9 @@ export type DialogReturnProps = { close: () => void } -export interface DialogComponent - extends ForwardRefExoticComponent>> { +export interface DialogComponent extends ForwardRefExoticComponent< + PropsWithChildren> +> { confirm: (props: Partial) => DialogReturnProps alert: (props: Partial) => DialogReturnProps config: (config: DialogConfigType) => void