diff --git a/packages/nutui-taro-demo/src/app.config.ts b/packages/nutui-taro-demo/src/app.config.ts index 7357a2dbf7..e491b7473d 100644 --- a/packages/nutui-taro-demo/src/app.config.ts +++ b/packages/nutui-taro-demo/src/app.config.ts @@ -32,7 +32,8 @@ const subPackages = [ "pages/navbar/index", "pages/sidenavbar/index", "pages/tabbar/index", - "pages/tabs/index" + "pages/tabs/index", + "pages/verticaltabs/index" ] }, { diff --git a/src/config.json b/src/config.json index 471698fbc8..8cdad1820c 100644 --- a/src/config.json +++ b/src/config.json @@ -390,12 +390,23 @@ "version": "2.0.0", "name": "Tabs", "type": "component", - "cName": "选项卡切换", + "cName": "水平选项卡", "desc": "常用于平级区域大块内容的的收纳和展现,支持内嵌标签形式和渲染循环数据形式", "sort": 12, "show": true, "taro": true, "author": "oasis" + }, + { + "version": "2.0.0", + "name": "VerticalTabs", + "type": "component", + "cName": "垂直选项卡", + "desc": "常用于平级区域大块内容的的收纳和展现,支持内嵌标签形式和渲染循环数据形式", + "sort": 13, + "show": true, + "taro": true, + "author": "Alex.hxy" } ] }, diff --git a/src/packages/tabs/__test__/tabs.spec.tsx b/src/packages/tabs/__test__/tabs.spec.tsx index 466c1c068e..415720da89 100644 --- a/src/packages/tabs/__test__/tabs.spec.tsx +++ b/src/packages/tabs/__test__/tabs.spec.tsx @@ -15,7 +15,7 @@ test('base Tabs', () => { test('base tabs props', () => { const { container } = render( - + Tab 1 @@ -31,7 +31,7 @@ test('base tabs props', () => { test('base tabs props', () => { const { container } = render( - + Tab 1 @@ -144,7 +144,7 @@ test('base click', () => { test('click tab when have many tabs', async () => { const handleClick = vi.fn(() => {}) const { container } = render( - + Tab 1 diff --git a/src/packages/tabs/demo.taro.tsx b/src/packages/tabs/demo.taro.tsx index de862eaed3..605e9fd5b7 100644 --- a/src/packages/tabs/demo.taro.tsx +++ b/src/packages/tabs/demo.taro.tsx @@ -22,11 +22,6 @@ import Demo15 from './demos/taro/demo15' import Demo16 from './demos/taro/demo16' import Demo17 from './demos/taro/demo17' import Demo18 from './demos/taro/demo18' -import Demo19 from './demos/taro/demo19' -import Demo20 from './demos/taro/demo20' -import Demo21 from './demos/taro/demo21' -import Demo22 from './demos/taro/demo22' -import Demo23 from './demos/taro/demo23' const TabsDemo = () => { const [translated] = useTranslate({ @@ -40,10 +35,6 @@ const TabsDemo = () => { title2: '通过 value 匹配', title3: '数据异步渲染 3s', title4: '数量多,滚动操作', - title5: '左右布局', - title6: '左右布局-微笑曲线', - title12: '嵌套布局', - title13: '嵌套布局 2', title14: '滑动切换', title7: 'Title 字体尺寸:20px 12px', title8: '自定义标签栏', @@ -64,10 +55,6 @@ const TabsDemo = () => { title2: 'Match By Value', title3: 'Data Is Rendered Asynchronously For 3s', title4: 'A Large Number Of Scrolling Operations', - title5: 'Left And Right Layout', - title6: 'Left And Right Layout - Smile Curve', - title12: 'Tabs In Tabs', - title13: 'Tabs In Tabs 2', title14: 'Slide To Switch', title7: 'Title FontSize: 20px 12px', title8: 'Custom Tab Bar', @@ -118,20 +105,10 @@ const TabsDemo = () => { {translated.title4} - {translated.title4} 2 - - {translated.title5} - - {translated.title6} - - {translated.title12} - - {translated.title13} - {translated.title7} - + {translated.title8} - + ) diff --git a/src/packages/tabs/demo.tsx b/src/packages/tabs/demo.tsx index f9aad59b7a..b3249a81a4 100644 --- a/src/packages/tabs/demo.tsx +++ b/src/packages/tabs/demo.tsx @@ -18,11 +18,6 @@ import Demo15 from './demos/h5/demo15' import Demo16 from './demos/h5/demo16' import Demo17 from './demos/h5/demo17' import Demo18 from './demos/h5/demo18' -import Demo19 from './demos/h5/demo19' -import Demo20 from './demos/h5/demo20' -import Demo21 from './demos/h5/demo21' -import Demo22 from './demos/h5/demo22' -import Demo23 from './demos/h5/demo23' const TabsDemo = () => { const [translated] = useTranslate({ @@ -36,10 +31,6 @@ const TabsDemo = () => { title2: '通过 value 匹配', title3: '数据异步渲染 3s', title4: '数量多,滚动操作', - title5: '左右布局', - title6: '左右布局-微笑曲线', - title12: '嵌套布局', - title13: '嵌套布局2', title14: '滑动切换', title7: 'Title 字体尺寸:20px 12px', title8: '自定义标签栏', @@ -60,10 +51,6 @@ const TabsDemo = () => { title2: 'Match By Value', title3: 'Data Is Rendered Asynchronously For 3s', title4: 'A Large Number Of Scrolling Operations', - title5: 'Left And Right Layout', - title6: 'Left And Right Layout - Smile Curve', - title12: 'Tabs In Tabs', - title13: 'Tabs In Tabs 2', title14: 'Slide To Switch', title7: 'Title FontSize: 20px 12px', title8: 'Custom Tab Bar', @@ -111,20 +98,10 @@ const TabsDemo = () => {

{translated.title4}

-

{translated.title4} 2

- -

{translated.title5}

- -

{translated.title6}

- -

{translated.title12}

- -

{translated.title13}

-

{translated.title7}

- +

{translated.title8}

- + ) diff --git a/src/packages/tabs/demos/h5/demo17.tsx b/src/packages/tabs/demos/h5/demo17.tsx index ce7285600d..915423fc59 100644 --- a/src/packages/tabs/demos/h5/demo17.tsx +++ b/src/packages/tabs/demos/h5/demo17.tsx @@ -1,26 +1,34 @@ import React, { useState } from 'react' import { Tabs } from '@nutui/nutui-react' -const Demo17 = () => { - const [tab4value, setTab4value] = useState('0') - const list4 = Array.from(new Array(10).keys()) +const Demo22 = () => { + const [tab11value, setTab11value] = useState('0') + const [tab12value, setTab12value] = useState('0') return ( <> { - setTab4value(value) + setTab11value(value) }} - direction="vertical" + style={{ '--nutui-tabs-titles-font-size': '20px' }} > - {list4.map((item) => ( - - Tab {item} - - ))} + Tab 1 + Tab 2 + Tab 3 + + { + setTab12value(value) + }} + style={{ '--nutui-tabs-titles-font-size': '12px' }} + > + Tab 1 + Tab 2 + Tab 3 ) } -export default Demo17 +export default Demo22 diff --git a/src/packages/tabs/demos/h5/demo18.tsx b/src/packages/tabs/demos/h5/demo18.tsx index 8a749b8ca4..fb193d7379 100644 --- a/src/packages/tabs/demos/h5/demo18.tsx +++ b/src/packages/tabs/demos/h5/demo18.tsx @@ -1,26 +1,49 @@ import React, { useState } from 'react' import { Tabs } from '@nutui/nutui-react' +import { Star } from '@nutui/icons-react' -const Demo18 = () => { - const [tab5value, setTab5value] = useState('0') - const list5 = Array.from(new Array(2).keys()) +const Demo23 = () => { + const [tab7value, setTab7value] = useState('c1') + const list6 = [ + { + title: '自定义 1', + paneKey: 'c1', + icon: , + }, + { + title: '自定义 2', + paneKey: 'c2', + }, + { + title: '自定义 3', + paneKey: 'c3', + }, + ] return ( <> { - setTab5value(value) + value={tab7value} + title={() => { + return list6.map((item) => ( +
setTab7value(item.paneKey)} + className={`nut-tabs-titles-item ${tab7value === item.paneKey ? 'nut-tabs-titles-item-active' : ''}`} + key={item.paneKey} + > + {item.icon || null} + {item.title} + +
+ )) }} - direction="vertical" > - {list5.map((item) => ( - - Tab {item} + {list6.map((item) => ( + + {item.title} ))}
) } -export default Demo18 +export default Demo23 diff --git a/src/packages/tabs/demos/h5/demo19.tsx b/src/packages/tabs/demos/h5/demo19.tsx deleted file mode 100644 index 481dcdee42..0000000000 --- a/src/packages/tabs/demos/h5/demo19.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react' - -const Demo19 = () => { - const [tab6value, setTab6value] = useState('0') - const list5 = Array.from(new Array(2).keys()) - return ( - <> - { - setTab6value(value) - }} - activeType="smile" - direction="vertical" - > - {list5.map((item) => ( - - Tab {item} - - ))} - - - ) -} -export default Demo19 diff --git a/src/packages/tabs/demos/h5/demo22.tsx b/src/packages/tabs/demos/h5/demo22.tsx deleted file mode 100644 index 915423fc59..0000000000 --- a/src/packages/tabs/demos/h5/demo22.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react' - -const Demo22 = () => { - const [tab11value, setTab11value] = useState('0') - const [tab12value, setTab12value] = useState('0') - return ( - <> - { - setTab11value(value) - }} - style={{ '--nutui-tabs-titles-font-size': '20px' }} - > - Tab 1 - Tab 2 - Tab 3 - - { - setTab12value(value) - }} - style={{ '--nutui-tabs-titles-font-size': '12px' }} - > - Tab 1 - Tab 2 - Tab 3 - - - ) -} -export default Demo22 diff --git a/src/packages/tabs/demos/h5/demo23.tsx b/src/packages/tabs/demos/h5/demo23.tsx deleted file mode 100644 index fb193d7379..0000000000 --- a/src/packages/tabs/demos/h5/demo23.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react' -import { Star } from '@nutui/icons-react' - -const Demo23 = () => { - const [tab7value, setTab7value] = useState('c1') - const list6 = [ - { - title: '自定义 1', - paneKey: 'c1', - icon: , - }, - { - title: '自定义 2', - paneKey: 'c2', - }, - { - title: '自定义 3', - paneKey: 'c3', - }, - ] - return ( - <> - { - return list6.map((item) => ( -
setTab7value(item.paneKey)} - className={`nut-tabs-titles-item ${tab7value === item.paneKey ? 'nut-tabs-titles-item-active' : ''}`} - key={item.paneKey} - > - {item.icon || null} - {item.title} - -
- )) - }} - > - {list6.map((item) => ( - - {item.title} - - ))} -
- - ) -} -export default Demo23 diff --git a/src/packages/tabs/demos/taro/demo17.tsx b/src/packages/tabs/demos/taro/demo17.tsx index 72b6b5139e..20ed7c76fe 100644 --- a/src/packages/tabs/demos/taro/demo17.tsx +++ b/src/packages/tabs/demos/taro/demo17.tsx @@ -1,26 +1,33 @@ import React, { useState } from 'react' import { Tabs } from '@nutui/nutui-react-taro' -const Demo17 = () => { - const [tab4value, setTab4value] = useState('0') - const list4 = Array.from(new Array(10).keys()) +const Demo22 = () => { + const [tab1value, setTab1value] = useState('0') return ( <> { - setTab4value(value) + setTab1value(value) }} - direction="vertical" + style={{ '--nutui-tabs-titles-font-size': '20px' }} > - {list4.map((item) => ( - - Tab {item} - - ))} + Tab 1 + Tab 2 + Tab 3 + + { + setTab1value(value) + }} + style={{ '--nutui-tabs-titles-font-size': '12px' }} + > + Tab 1 + Tab 2 + Tab 3 ) } -export default Demo17 +export default Demo22 diff --git a/src/packages/tabs/demos/taro/demo18.tsx b/src/packages/tabs/demos/taro/demo18.tsx index 35bc59b23c..7e25c8d647 100644 --- a/src/packages/tabs/demos/taro/demo18.tsx +++ b/src/packages/tabs/demos/taro/demo18.tsx @@ -1,26 +1,50 @@ import React, { useState } from 'react' +import { View, Text } from '@tarojs/components' import { Tabs } from '@nutui/nutui-react-taro' +import { Star } from '@nutui/icons-react-taro' -const Demo18 = () => { - const [tab5value, setTab5value] = useState('0') - const list5 = Array.from(new Array(2).keys()) +const Demo23 = () => { + const [tab7value, setTab7value] = useState('c1') + const list6 = [ + { + title: '自定义 1', + paneKey: 'c1', + icon: , + }, + { + title: '自定义 2', + paneKey: 'c2', + }, + { + title: '自定义 3', + paneKey: 'c3', + }, + ] return ( <> { - setTab5value(value) + value={tab7value} + title={() => { + return list6.map((item) => ( + setTab7value(item.paneKey)} + className={`nut-tabs-titles-item ${tab7value === item.paneKey ? 'nut-tabs-titles-item-active' : ''}`} + key={item.paneKey} + > + {item.icon || null} + {item.title} + + + )) }} - direction="vertical" > - {list5.map((item) => ( - - Tab {item} + {list6.map((item) => ( + + {item.title} ))} ) } -export default Demo18 +export default Demo23 diff --git a/src/packages/tabs/demos/taro/demo19.tsx b/src/packages/tabs/demos/taro/demo19.tsx deleted file mode 100644 index 04a7dd0328..0000000000 --- a/src/packages/tabs/demos/taro/demo19.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react-taro' - -const Demo19 = () => { - const [tab6value, setTab6value] = useState('0') - const list5 = Array.from(new Array(2).keys()) - return ( - <> - { - setTab6value(value) - }} - activeType="smile" - direction="vertical" - > - {list5.map((item) => ( - - Tab {item} - - ))} - - - ) -} -export default Demo19 diff --git a/src/packages/tabs/demos/taro/demo20.tsx b/src/packages/tabs/demos/taro/demo20.tsx deleted file mode 100644 index 7395364061..0000000000 --- a/src/packages/tabs/demos/taro/demo20.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react-taro' - -const Demo20 = () => { - const [tab8value, setTab8value] = useState('0') - const [tab9value, setTab9value] = useState('0') - return ( - <> - { - setTab8value(value) - }} - direction="vertical" - > - - { - setTab9value(value) - }} - direction="horizontal" - > - Tab 1 - Tab 2 - Tab 3 - - - Tab 2 - Tab 3 - - - ) -} -export default Demo20 diff --git a/src/packages/tabs/demos/taro/demo22.tsx b/src/packages/tabs/demos/taro/demo22.tsx deleted file mode 100644 index 20ed7c76fe..0000000000 --- a/src/packages/tabs/demos/taro/demo22.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react-taro' - -const Demo22 = () => { - const [tab1value, setTab1value] = useState('0') - return ( - <> - { - setTab1value(value) - }} - style={{ '--nutui-tabs-titles-font-size': '20px' }} - > - Tab 1 - Tab 2 - Tab 3 - - { - setTab1value(value) - }} - style={{ '--nutui-tabs-titles-font-size': '12px' }} - > - Tab 1 - Tab 2 - Tab 3 - - - ) -} -export default Demo22 diff --git a/src/packages/tabs/demos/taro/demo23.tsx b/src/packages/tabs/demos/taro/demo23.tsx deleted file mode 100644 index 7e25c8d647..0000000000 --- a/src/packages/tabs/demos/taro/demo23.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useState } from 'react' -import { View, Text } from '@tarojs/components' -import { Tabs } from '@nutui/nutui-react-taro' -import { Star } from '@nutui/icons-react-taro' - -const Demo23 = () => { - const [tab7value, setTab7value] = useState('c1') - const list6 = [ - { - title: '自定义 1', - paneKey: 'c1', - icon: , - }, - { - title: '自定义 2', - paneKey: 'c2', - }, - { - title: '自定义 3', - paneKey: 'c3', - }, - ] - return ( - <> - { - return list6.map((item) => ( - setTab7value(item.paneKey)} - className={`nut-tabs-titles-item ${tab7value === item.paneKey ? 'nut-tabs-titles-item-active' : ''}`} - key={item.paneKey} - > - {item.icon || null} - {item.title} - - - )) - }} - > - {list6.map((item) => ( - - {item.title} - - ))} - - - ) -} -export default Demo23 diff --git a/src/packages/tabs/doc.en-US.md b/src/packages/tabs/doc.en-US.md index fd66b26721..795a39704f 100644 --- a/src/packages/tabs/doc.en-US.md +++ b/src/packages/tabs/doc.en-US.md @@ -142,53 +142,11 @@ When autoHeight is set to true, nut-tabs and nut-tabs\_\_content will change wit ::: -::: - -### A Large Number Of Scrolling Operations 2 - -:::demo - - - -::: - -### Left And Right Layout - -:::demo - - - -::: - -### Left And Right Layout-Smile Curve - -:::demo - - - -::: - -### Tabs In Tabs - -:::demo - - - -::: - -### Tabs In Tabs 2 - -:::demo - - - -::: - ### Title FontSize: 20px 12px :::demo - + ::: @@ -196,7 +154,7 @@ When autoHeight is set to true, nut-tabs and nut-tabs\_\_content will change wit :::demo - + ::: @@ -209,7 +167,6 @@ When autoHeight is set to true, nut-tabs and nut-tabs\_\_content will change wit | value | The value of the currently active tab panel | `number` \| `string` | `0` | | defaultValue | Initialize the value of the active tab | `number` \| `string` | `0` | | activeColor | Label selected color | `string` | `#1A1A1A` | -| direction | Use horizontal and vertical directions | `horizontal` \| `vertical` | `horizontal` | | activeType | Select the bottom display style Optional values `line`、`smile`、`simple`、`card`、`button`、`divider` | `line` \| `smile` \| `simple` \| `card` \| `button`\| `divider` | `line` | | duration | Switch animation duration, unit ms 0 means no animation | `number` \| `string` | `300` | | title | custom navigation area | `() => JSX.Element[]` | `-` | diff --git a/src/packages/tabs/doc.md b/src/packages/tabs/doc.md index e1677c9116..0005e23efe 100644 --- a/src/packages/tabs/doc.md +++ b/src/packages/tabs/doc.md @@ -142,53 +142,11 @@ import { Tabs } from '@nutui/nutui-react' ::: -::: - -### 数量多,滚动操作2 - -:::demo - - - -::: - -### 左右布局 - -:::demo - - - -::: - -### 左右布局-微笑曲线 - -:::demo - - - -::: - -### 嵌套布局 - -:::demo - - - -::: - -### 嵌套布局2 - -:::demo - - - -::: - ### Title 字体尺寸:20px 12px :::demo - + ::: @@ -196,7 +154,7 @@ import { Tabs } from '@nutui/nutui-react' :::demo - + ::: @@ -209,7 +167,6 @@ import { Tabs } from '@nutui/nutui-react' | value | 当前激活 tab 面板的值 | `number` \| `string` | `0` | | defaultValue | 初始化激活 tab 的值 | `number` \| `string` | `0` | | activeColor | 标签选中色 | `string` | `#1A1A1A` | -| direction | 使用横纵方向 | `horizontal` \| `vertical` | `horizontal` | | activeType | 选中底部展示样式 可选值 `line`、`smile`、`simple`、`card`、`button`、`divider` | `line` \| `smile` \| `simple` \| `card` \| `button`\| `divider` | `line` | | duration | 切换动画时长,单位 ms 0 代表无动画 | `number` \| `string` | `300` | | title | 自定义导航区域 | `() => JSX.Element[]` | `-` | diff --git a/src/packages/tabs/doc.taro.md b/src/packages/tabs/doc.taro.md index 82f5706924..f75e6fa76d 100644 --- a/src/packages/tabs/doc.taro.md +++ b/src/packages/tabs/doc.taro.md @@ -142,51 +142,11 @@ import { Tabs } from '@nutui/nutui-react-taro' ::: -### 数量多,滚动操作 2 - -:::demo - - - -::: - -### 左右布局 - -:::demo - - - -::: - -### 左右布局-微笑曲线 - -:::demo - - - -::: - -### 嵌套布局 - -:::demo - - - -::: - -### 嵌套布局 2 - -:::demo - - - -::: - ### Title 字体尺寸: 20px 12px :::demo - + ::: @@ -194,7 +154,7 @@ import { Tabs } from '@nutui/nutui-react-taro' :::demo - + ::: @@ -207,7 +167,6 @@ import { Tabs } from '@nutui/nutui-react-taro' | value | 当前激活 tab 面板的值 | `number` \| `string` | `0` | | defaultValue | 初始化激活 tab 的值 | `number` \| `string` | `0` | | activeColor | 标签选中色 | `string` | `#1A1A1A` | -| direction | 使用横纵方向 | `horizontal` \| `vertical` | `horizontal` | | activeType | 选中底部展示样式 可选值 `line`、`smile`、`simple`、`card`、`button`、`divider` | `line` \| `smile` \| `simple` \| `card` \| `button`\| `divider` | `line` | | duration | 切换动画时长,单位 ms 0 代表无动画 | `number` \| `string` | `300` | | title | 自定义导航区域 | `() => JSX.Element[]` | `-` | diff --git a/src/packages/tabs/doc.zh-TW.md b/src/packages/tabs/doc.zh-TW.md index 2211e055a7..d69de776b3 100644 --- a/src/packages/tabs/doc.zh-TW.md +++ b/src/packages/tabs/doc.zh-TW.md @@ -142,51 +142,11 @@ import { Tabs } from '@nutui/nutui-react' ::: -### 數量多,滾動操作2 - -:::demo - - - -::: - -### 左右布局 - -:::demo - - - -::: - -### 左右布局-微笑曲線 - -:::demo - - - -::: - -### 嵌套布局 - -:::demo - - - -::: - -### 嵌套布局2 - -:::demo - - - -::: - ### Title 字體尺寸:20px 12px :::demo - + ::: @@ -194,7 +154,7 @@ import { Tabs } from '@nutui/nutui-react' :::demo - + ::: @@ -207,7 +167,6 @@ import { Tabs } from '@nutui/nutui-react' | value | 當前激活 tab 面板的值 | `number` \| `string` | `0` | | defaultValue | 初始化激活 tab 的值 | `number` \| `string` | `0` | | activeColor | 標簽選中色 | `string` | `#1A1A1A` | -| direction | 使用橫縱方向 | `horizontal` \| `vertical` | `horizontal` | | activeType | 選中底部展示樣式 可選值 `line`、`smile`、`simple`、`card`、`button`、`divider` | `line` \| `smile` \| `simple` \| `card` \| `button`\| `divider` | `line` | | duration | 切換動畫時長,單位 ms 0 代表無動畫 | `number` \| `string` | `300` | | title | 自定義導航區域 | `() => JSX.Element[]` | `-` | diff --git a/src/packages/tabs/index.taro.ts b/src/packages/tabs/index.taro.ts index 73fe03e706..9fc56f570d 100644 --- a/src/packages/tabs/index.taro.ts +++ b/src/packages/tabs/index.taro.ts @@ -1,4 +1,4 @@ import { Tabs } from './tabs.taro' -export type { TabsProps, TabsTitle } from './tabs.taro' +export type { TabsProps, TabsTitle } from './type' export default Tabs diff --git a/src/packages/tabs/index.ts b/src/packages/tabs/index.ts index 4aea9074ab..e224d32d7a 100644 --- a/src/packages/tabs/index.ts +++ b/src/packages/tabs/index.ts @@ -1,4 +1,4 @@ import { Tabs } from './tabs' -export type { TabsProps, TabsTitle } from './tabs' +export type { TabsProps, TabsTitle } from './type' export default Tabs diff --git a/src/packages/tabs/tabs.scss b/src/packages/tabs/tabs.scss index e5c4a3a834..f76dcc827f 100644 --- a/src/packages/tabs/tabs.scss +++ b/src/packages/tabs/tabs.scss @@ -13,34 +13,29 @@ overflow: hidden; background: $tabs-titles-background-color; scrollbar-width: none; - &::-webkit-scrollbar { display: none; width: 0; background: transparent; } - .nut-tabs-list { width: 100%; height: auto; display: flex; flex-shrink: 0; } - &-left { justify-content: flex-start; .nut-tabs-titles-item { padding: 0 22px; } } - &-right { justify-content: flex-end; .nut-tabs-titles-item { padding: 0 22px; } } - &-scrollable { overflow-x: auto; overflow-y: hidden; @@ -65,11 +60,9 @@ &-right { flex: none; } - &-text { text-align: center; } - &-smile, &-line { position: absolute; @@ -87,7 +80,6 @@ &-smile { bottom: $tabs-titles-item-smile-bottom; - .nut-icon { position: absolute; font-size: 20px; @@ -100,7 +92,6 @@ &-active { color: $tabs-titles-item-active-color; font-weight: $tabs-titles-item-active-font-weight; - .nut-tabs-titles-item-line { overflow: unset; content: ' '; @@ -108,14 +99,12 @@ height: $tabs-tab-line-height; background: $tabs-tab-line-color; } - .nut-tabs-titles-item-smile { overflow: unset; width: 40px; height: 20px; } } - &-disabled { color: $color-text-disabled; } @@ -131,10 +120,8 @@ &-card { padding: 0; background-color: $color-default-light; - .nut-tabs-titles-item { padding: 0; - &-active { font-weight: $font-weight-bold; background-color: $white; @@ -158,11 +145,9 @@ .nut-tabs-titles-item-active { .nut-tabs-titles-item-text { background: $color-default-light; - color: $color-text; border-radius: $tabs-tab-button-border-radius; font-weight: $font-weight-bold; background-color: $tabs-tab-button-active-background-color; - color: $color-primary; border: $tabs-tab-button-active-border; } } @@ -171,11 +156,9 @@ &-divider { padding: 0; border-bottom: 1px solid $color-border; - .nut-tabs-titles-item { padding: 0; position: relative; - &::after { content: ''; position: absolute; @@ -186,7 +169,6 @@ background: $color-border; transform: translateY(-50%); } - &:last-child { &::after { display: none; @@ -221,212 +203,9 @@ position: relative; } -.nut-tabs-vertical { - flex-direction: row; - width: 100%; - - .nut-tabs-ellipsis { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - .nut-tabs-titles { - box-sizing: border-box; - flex-direction: column; - height: 100%; - padding: 0; - width: $tabs-vertical-titles-width; - flex-shrink: 0; - - .nut-tabs-list { - width: 100%; - display: flex; - flex-direction: column; - flex-shrink: 0; - } - - &-line { - .nut-tabs-titles-item { - padding-left: 14px; - } - } - } - - .nut-tabs-titles-scrollable { - overflow-x: hidden; - overflow-y: auto; - } - - .nut-tabs-titles-item { - height: $tabs-vertical-titles-item-height; - margin: 0; - flex: none; - - &-smile { - width: 0; - height: 0; - overflow: hidden; - transition: width 0.3s ease; - } - - &-line { - width: 0; - height: 0; - transform: translate(0, -50%); - transition: height 0.3s ease; - - &-vertical { - top: 50%; - } - } - - &-active { - background-color: $tabs-titles-item-active-background-color; - .nut-tabs-titles-item-line { - left: 10px; - width: $tabs-vertical-tab-line-width; - height: $tabs-vertical-tab-line-height; - background: $tabs-vertical-tab-line-color; - } - - .nut-tabs-titles-item-smile { - right: -12px; - bottom: -2%; - left: auto; - width: 40px; - height: 20px; - transform: rotate(320deg); - } - } - } - - .nut-tabs-horizontal { - .nut-tabs-titles { - flex-direction: row; - overflow-x: auto; - overflow-y: hidden; - height: $tabs-titles-height; - - .nut-tabs-list { - width: 100%; - display: flex; - flex-direction: row; - flex-shrink: 0; - } - } - - .nut-tabs-content { - flex-direction: row; - } - - .nut-tabs-titles-item-active { - background-color: initial; - - .nut-tabs-titles-item-line { - left: 50%; - transform: translate(-50%, 0); - } - - .nut-tabs-titles-item-smile { - left: 50%; - right: auto; - bottom: -3px; - transform: translate(-50%, 0) rotate(0deg); - } - } - } - - .nut-tabs-content { - flex-direction: column; - height: 100%; - - &-wrap { - flex: 1; - } - - .nut-tabpane { - height: 100%; - } - } - - .nut-tabs-horizontal { - .nut-tabs-titles { - display: flex; - flex-direction: row; - padding: 0 !important; - width: 100%; - - .nut-tabs-titles-item { - display: flex; - align-items: center; - justify-content: center; - flex: 1 0 auto; - - &-active { - color: $color-primary; - font-weight: $tabs-titles-item-active-font-weight; - font-size: $tabs-titles-item-active-font-size; - - .nut-tabs-titles-item-line { - content: ' '; - width: $tabs-tab-line-width; - height: $tabs-tab-line-height; - background: $tabs-tab-line-color; - } - } - } - } - } -} - -[dir='rtl'] .nut-tabs-vertical, -.nut-rtl .nut-tabs-vertical { - .nut-tabs-titles { - &-line { - .nut-tabs-titles-item { - padding-left: 0; - padding-right: 14px; - } - } - } - - .nut-tabs-titles-item { - &-active { - .nut-tabs-titles-item-line { - left: auto; - right: 10px; - } - - .nut-tabs-titles-item-smile { - left: -12px; - right: auto; - transform: rotate(-320deg); - } - } - } - - .nut-tabs-horizontal { - .nut-tabs-titles-item-active { - .nut-tabs-titles-item-line { - left: auto; - right: 50%; - transform: translate(50%, 0); - } - - .nut-tabs-titles-item-smile { - right: 50%; - left: auto; - transform: translate(50%, 0) rotate(0deg); - } - } - } -} - .nut-tabs-content { display: flex; box-sizing: border-box; - &-wrap { overflow: hidden; } diff --git a/src/packages/tabs/tabs.taro.tsx b/src/packages/tabs/tabs.taro.tsx index 6638e50c45..bfef20f66e 100644 --- a/src/packages/tabs/tabs.taro.tsx +++ b/src/packages/tabs/tabs.taro.tsx @@ -3,49 +3,26 @@ import { ScrollView, View } from '@tarojs/components' import classNames from 'classnames' import { JoySmile } from '@nutui/icons-react-taro' import Taro, { nextTick, createSelectorQuery } from '@tarojs/taro' -import { BasicComponent, ComponentDefaults } from '@/utils/typings' +import { ComponentDefaults } from '@/utils/typings' import TabPane from '@/packages/tabpane/index.taro' import { usePropsValue } from '@/utils/use-props-value' import { useForceUpdate } from '@/utils/use-force-update' import raf from '@/utils/raf' import useUuid from '@/utils/use-uuid' import { useRtl } from '../configprovider/configprovider.taro' - -export type TabsTitle = { - title: string - disabled: boolean - active?: boolean - value: string | number -} - -export interface TabsProps extends BasicComponent { - tabStyle: React.CSSProperties - value: string | number - defaultValue: string | number - activeColor: string - name: string - direction: 'horizontal' | 'vertical' - activeType: 'line' | 'smile' | 'simple' | 'card' | 'button' | 'divider' - duration: number | string - align: 'left' | 'right' - title: () => JSX.Element[] - onChange: (index: string | number) => void - onClick: (index: string | number) => void - autoHeight: boolean - children?: React.ReactNode -} +import { mergeProps } from '@/utils/merge-props' +import { TabsTitle, TabsProps, RectItem } from './type' const defaultProps = { ...ComponentDefaults, tabStyle: {}, activeColor: '', - direction: 'horizontal', activeType: 'line', duration: 300, autoHeight: false, } as TabsProps - const classPrefix = 'nut-tabs' + export const Tabs: FunctionComponent> & { TabPane: typeof TabPane } = (props) => { @@ -53,7 +30,6 @@ export const Tabs: FunctionComponent> & { const { activeColor, tabStyle, - direction, activeType, duration, align, @@ -67,22 +43,16 @@ export const Tabs: FunctionComponent> & { value: outerValue, defaultValue: outerDefaultValue, ...rest - } = { - ...defaultProps, - ...props, - } + } = mergeProps(defaultProps, props) const uuid = useUuid() - const [value, setValue] = usePropsValue({ value: outerValue, defaultValue: outerDefaultValue, finalValue: 0, onChange, }) - const titleItemsRef = useRef([]) const navRef = useRef(null) - const getTitles = () => { const titles: TabsTitle[] = [] React.Children.forEach(children, (child: any, idx) => { @@ -99,7 +69,6 @@ export const Tabs: FunctionComponent> & { }) return titles } - const titles = useRef(getTitles()) const forceUpdate = useForceUpdate() useEffect(() => { @@ -118,7 +87,7 @@ export const Tabs: FunctionComponent> & { }, [children]) const classes = classNames( classPrefix, - `${classPrefix}-${direction}`, + `${classPrefix}-horizontal`, className ) const classesTitle = classNames(`${classPrefix}-titles`, { @@ -126,7 +95,6 @@ export const Tabs: FunctionComponent> & { [`${classPrefix}-titles-scrollable`]: true, [`${classPrefix}-titles-${align}`]: align, }) - const tabsActiveStyle = { color: activeType === 'smile' ? activeColor : '', background: activeType === 'line' ? activeColor : '', @@ -151,39 +119,20 @@ export const Tabs: FunctionComponent> & { }) }) } - type RectItem = { - bottom: number - dataset: { sid: string } - height: number - id: string - left: number - right: number - top: number - width: number - } + const scrollWithAnimation = useRef(false) const navRectRef = useRef() const titleRectRef = useRef([]) const [scrollLeft, setScrollLeft] = useState(0) - const [scrollTop, setScrollTop] = useState(0) - const scrollDirection = ( - to: number, - direction: 'horizontal' | 'vertical' - ) => { + const scrollDirection = (to: number) => { let count = 0 const frames = 1 - function animate() { - if (direction === 'horizontal') { - setScrollLeft(to) - } else { - setScrollTop(to) - } + setScrollLeft(to) if (++count < frames) { raf(animate) } } - animate() } const scrollIntoView = (index: number) => { @@ -194,44 +143,30 @@ export const Tabs: FunctionComponent> & { ]).then(([navRect, titleRects]: any) => { navRectRef.current = navRect titleRectRef.current = titleRects - // @ts-ignore const titleRect: RectItem = titleRectRef.current[index] if (!titleRect) return - let to = 0 - if (direction === 'vertical') { - const top = titleRects - .slice(0, index) - .reduce((prev: number, curr: RectItem) => prev + curr.height, 0) - to = top - (navRectRef.current.height - titleRect.height) / 2 - } else { - const left = titleRects - .slice(0, index) - .reduce((prev: number, curr: RectItem) => prev + curr.width, 0) - to = left - (navRectRef.current.width - titleRect.width) / 2 - // to < 0 说明不需要进行滚动,页面元素已全部显示出来 - if (to < 0) return - to = rtl ? -to : to - } + const left = titleRects + .slice(0, index) + .reduce((prev: number, curr: RectItem) => prev + curr.width, 0) + to = left - (navRectRef.current.width - titleRect.width) / 2 + // to < 0 说明不需要进行滚动,页面元素已全部显示出来 + if (to < 0) return + to = rtl ? -to : to nextTick(() => { scrollWithAnimation.current = true }) - - scrollDirection(to, direction) + scrollDirection(to) }) }) } - const getContentStyle = () => { let index = titles.current.findIndex( (t) => String(t.value) === String(value) ) index = index < 0 ? 0 : index return { - transform: - direction === 'horizontal' - ? `translate3d(${rtl ? '' : '-'}${index * 100}%, 0, 0)` - : `translate3d( 0,-${index * 100}%, 0)`, + transform: `translate3d(${rtl ? '' : '-'}${index * 100}%, 0, 0)`, transitionDuration: `${duration}ms`, } } @@ -246,9 +181,7 @@ export const Tabs: FunctionComponent> & { const tabChange = (item: TabsTitle, index: number) => { onClick && onClick(item.value) - if (item.disabled) { - return - } + if (item.disabled) return setValue(item.value) } @@ -256,10 +189,9 @@ export const Tabs: FunctionComponent> & { > & { className={classNames( `${classPrefix}-titles-item-line`, { - [`${classPrefix}-titles-item-line-${direction}`]: + [`${classPrefix}-titles-item-line-horizontal`]: true, } )} @@ -328,12 +260,10 @@ export const Tabs: FunctionComponent> & { if (!React.isValidElement(child)) { return null } - let childProps = { ...child.props, active: value === child.props.value, } - if ( String(value) !== String(child.props.value || idx) && autoHeight diff --git a/src/packages/tabs/tabs.tsx b/src/packages/tabs/tabs.tsx index 103cbdb4a8..06282901f4 100644 --- a/src/packages/tabs/tabs.tsx +++ b/src/packages/tabs/tabs.tsx @@ -1,41 +1,18 @@ import React, { FunctionComponent, useEffect, useRef } from 'react' import classNames from 'classnames' import { JoySmile } from '@nutui/icons-react' -import { BasicComponent, ComponentDefaults } from '@/utils/typings' +import { ComponentDefaults } from '@/utils/typings' import TabPane from '@/packages/tabpane' import raf from '@/utils/raf' import { usePropsValue } from '@/utils/use-props-value' import { useForceUpdate } from '@/utils/use-force-update' import { useRtl } from '../configprovider' - -export type TabsTitle = { - title: string - disabled: boolean - active?: boolean - value: string | number -} - -export interface TabsProps extends BasicComponent { - tabStyle: React.CSSProperties - value: string | number - defaultValue: string | number - activeColor: string - direction: 'horizontal' | 'vertical' - activeType: 'line' | 'smile' | 'simple' | 'card' | 'button' | 'divider' - duration: number | string - align: 'left' | 'right' - title: () => JSX.Element[] - onChange: (index: string | number) => void - onClick: (index: string | number) => void - autoHeight: boolean - children?: React.ReactNode -} +import { TabsProps, TabsTitle } from './type' const defaultProps = { ...ComponentDefaults, tabStyle: {}, activeColor: '', - direction: 'horizontal', activeType: 'line', duration: 300, autoHeight: false, @@ -49,7 +26,6 @@ export const Tabs: FunctionComponent> & { const { activeColor, tabStyle, - direction, activeType, duration, align, @@ -75,23 +51,13 @@ export const Tabs: FunctionComponent> & { }) const titleItemsRef = useRef([]) const navRef = useRef(null) - const scrollDirection = ( - nav: any, - to: number, - duration: number, - direction?: 'horizontal' | 'vertical' - ) => { + const scrollDirection = (nav: any, to: number, duration: number) => { let count = 0 - const from = direction === 'horizontal' ? nav.scrollLeft : nav.scrollTop + const from = nav.scrollLeft const frames = duration === 0 ? 1 : Math.round((duration * 1000) / 16) function animate() { - if (direction === 'horizontal') { - nav.scrollLeft += (to - from) / frames - } else { - nav.scrollTop += (to - from) / frames - } - + nav.scrollLeft += (to - from) / frames if (++count < frames) { raf(animate) } @@ -103,19 +69,12 @@ export const Tabs: FunctionComponent> & { const titleItem = titleItemsRef.current const titlesLength = titles.current.length const itemLength = titleItemsRef.current.length - if (!nav || !titleItem || !titleItem[itemLength - titlesLength + index]) { + if (!nav || !titleItem || !titleItem[itemLength - titlesLength + index]) return - } - const title = titleItem[itemLength - titlesLength + index] - let to = 0 - if (direction === 'vertical') { - const runTop = title.offsetTop - nav.offsetTop + 10 - to = runTop - (nav.offsetHeight - title.offsetHeight) / 2 - } else { - to = title.offsetLeft - (nav.offsetWidth - title.offsetWidth) / 2 - } - scrollDirection(nav, to, immediate ? 0 : 0.3, direction) + const title = titleItem[itemLength - titlesLength + index] + const to = title.offsetLeft - (nav.offsetWidth - title.offsetWidth) / 2 + scrollDirection(nav, to, immediate ? 0 : 0.3) } const getTitles = () => { @@ -140,7 +99,6 @@ export const Tabs: FunctionComponent> & { titles.current = getTitles() let current: string | number = '' titles.current.forEach((title) => { - // eslint-disable-next-line eqeqeq if (title.value === value) { current = value } @@ -154,7 +112,7 @@ export const Tabs: FunctionComponent> & { const classes = classNames( classPrefix, - `${classPrefix}-${direction}`, + `${classPrefix}-horizontal`, className ) const classesTitle = classNames(`${classPrefix}-titles`, { @@ -168,20 +126,15 @@ export const Tabs: FunctionComponent> & { background: activeType === 'line' ? activeColor : '', } const getContentStyle = () => { - // eslint-disable-next-line eqeqeq - let index = titles.current.findIndex((t) => t.value == value) + let index = titles.current.findIndex((t) => t.value === value) index = index < 0 ? 0 : index return { - transform: - direction === 'horizontal' - ? `translate3d(${rtl ? '' : '-'}${index * 100}%, 0, 0)` - : `translate3d( 0,-${index * 100}%, 0)`, + transform: `translate3d(${rtl ? '' : '-'}${index * 100}%, 0, 0)`, transitionDuration: `${duration}ms`, } } useEffect(() => { - // eslint-disable-next-line eqeqeq - let index = titles.current.findIndex((t) => t.value == value) + let index = titles.current.findIndex((t) => t.value === value) index = index < 0 ? 0 : index setTimeout(() => { scrollIntoView(index) @@ -218,7 +171,7 @@ export const Tabs: FunctionComponent> & { {activeType === 'line' && (
@@ -249,12 +202,10 @@ export const Tabs: FunctionComponent> & { if (!React.isValidElement(child)) { return null } - let childProps = { ...child.props, active: value === child.props.value, } - if ( String(value) !== String(child.props.value || idx) && autoHeight diff --git a/src/packages/tabs/type.ts b/src/packages/tabs/type.ts new file mode 100644 index 0000000000..0badd9a962 --- /dev/null +++ b/src/packages/tabs/type.ts @@ -0,0 +1,34 @@ +import { BasicComponent } from '@/utils/typings' + +export type TabsTitle = { + title: string + disabled: boolean + active?: boolean + value: string | number +} + +export interface TabsProps extends BasicComponent { + tabStyle: React.CSSProperties + value: string | number + defaultValue: string | number + activeColor: string + activeType: 'line' | 'smile' | 'simple' | 'card' | 'button' | 'divider' + duration: number | string + align: 'left' | 'right' + name?: string + title: () => JSX.Element[] + onChange: (index: string | number) => void + onClick: (index: string | number) => void + autoHeight: boolean + children?: React.ReactNode +} +export type RectItem = { + bottom: number + dataset: { sid: string } + height: number + id: string + left: number + right: number + top: number + width: number +} diff --git a/src/packages/verticaltabs/__test__/verticaltabs.spec.tsx b/src/packages/verticaltabs/__test__/verticaltabs.spec.tsx new file mode 100644 index 0000000000..6e0f64c097 --- /dev/null +++ b/src/packages/verticaltabs/__test__/verticaltabs.spec.tsx @@ -0,0 +1,172 @@ +import * as React from 'react' +import { render, waitFor, fireEvent } from '@testing-library/react' +import '@testing-library/jest-dom' +import { VerticalTabs as Tabs } from '../verticaltabs' +import { TabPane } from '../../tabpane/tabpane' + +test('base Tabs', () => { + const { getByTestId } = render( + + Tab 1 + + ) + expect(getByTestId('tabs1')).toHaveClass('nut-tabs') +}) + +test('base tabs props', () => { + const { container } = render( + + + Tab 1 + + + ) + const el2 = container.querySelectorAll('.nut-tabs-vertical') + const el3 = container.querySelectorAll('.nut-tabs-titles')[0] + + expect(el2.length > 0).toBe(true) + expect(el3).toHaveClass('nut-tabs-titles-smile') + expect(el3).toHaveClass('nut-tabs-titles-scrollable') +}) + +test('base tabs props', () => { + const { container } = render( + + + Tab 1 + + + ) + const el3 = container.querySelectorAll('.nut-tabs-titles')[0] + expect(el3).toHaveClass('nut-tabs-titles-card') +}) + +test('base other props', async () => { + const { container } = render( + + Tab 1 + Tab 2 + + ) + + const el: Element | null = container.querySelector('.nut-tabs-content') + expect(el).toHaveAttribute( + 'style', + 'transform: translate3d( 0,-0%, 0); transition-duration: 500ms;' + ) + const el2 = container.querySelectorAll('.nut-tabs-titles-item')[1] + fireEvent.click(el2) + let el3: Element | null + setTimeout(() => { + el3 = container.querySelector('.nut-tabs-content') + }, 600) + await waitFor(() => { + expect(el3).toHaveAttribute( + 'style', + 'transform: translate3d( 0,-100%, 0); transition-duration: 500ms;' + ) + }) +}) + +test('base Tabpane Props', () => { + const { container } = render( + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + ) + const el = container.querySelectorAll('.nut-tabs-titles-item') + const el2 = container.querySelectorAll('.nut-tabs-titles-item-text') + expect(el.length === 3).toBe(true) + expect(el[0]).toHaveClass('nut-tabs-titles-item-active') + expect(el[1]).toHaveClass('nut-tabs-titles-item-disabled') + expect(el2[0]).toHaveTextContent('Tab 1') +}) + +test('base Tabpane autoHeight Props', () => { + const { container } = render( + + + Tab 1 + + + Tab 2 + + + ) + const el = container.querySelectorAll('.nut-tabpane') + expect(el[1]).toHaveClass('nut-tabpane inactive') +}) + +test('base children isnot valid element', () => { + const handleClick = vi.fn(() => {}) + const { container } = render( + + 333 + + ) + const el = container.querySelectorAll('.nut-tabs-content')[0].children + expect(el.length).toBe(0) +}) + +test('base click', () => { + const handleClick = vi.fn(() => {}) + const { container } = render( + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + ) + + const el = container.querySelectorAll('.nut-tabs-titles-item')[0] + fireEvent.click(el) + expect(handleClick).toBeCalled() + + const el2 = container.querySelectorAll('.nut-tabs-titles-item')[1] + fireEvent.click(el2) + expect(handleClick).toBeCalled() +}) + +test('click tab when have many tabs', async () => { + const handleClick = vi.fn(() => {}) + const { container } = render( + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + Tab 11 + + + Tab 22 + + + Tab 33 + + + ) + + const el = container.querySelectorAll('.nut-tabs-titles-item')[5] + fireEvent.click(el) + await waitFor(() => expect(expect(handleClick).toBeCalled())) +}) diff --git a/src/packages/verticaltabs/demo.taro.tsx b/src/packages/verticaltabs/demo.taro.tsx new file mode 100644 index 0000000000..b7fb85870e --- /dev/null +++ b/src/packages/verticaltabs/demo.taro.tsx @@ -0,0 +1,68 @@ +import React from 'react' +import Taro from '@tarojs/taro' +import { ScrollView, View } from '@tarojs/components' +import { useTranslate } from '@/sites/assets/locale/taro' +import Header from '@/sites/components/header' +import Demo1 from './demos/taro/demo1' +import Demo2 from './demos/taro/demo2' +import Demo3 from './demos/taro/demo3' +import Demo4 from './demos/taro/demo4' +import Demo5 from './demos/taro/demo5' +import Demo6 from './demos/taro/demo6' +import Demo7 from './demos/taro/demo7' +import Demo8 from './demos/taro/demo8' + +const TabsDemo = () => { + const [translated] = useTranslate({ + 'zh-CN': { + basic: '基础用法', + title1: '基础用法-微笑曲线', + titleLite: '基础用法-简约模式', + titleCard: '基础用法-卡片模式', + titleButton: '基础用法-按钮模式', + titleDivider: '基础用法-分割线模式', + title4: '数量多,滚动操作', + title12: '嵌套布局', + title13: '嵌套布局2', + }, + 'en-US': { + basic: 'Basic Usage', + title1: 'Basic Usage - Smile Curve', + titleLite: 'Basic Usage - Simple Mode', + titleCard: 'Basic Usage - Card Mode', + titleButton: 'Basic Usage - Button Mode', + titleDivider: 'Basic Usage - Divider Mode', + title4: 'A Large Number Of Scrolling Operations', + title12: 'Tabs In Tabs', + title13: 'Tabs In Tabs 2', + }, + }) + + return ( + <> +
+ + {translated.basic} + + {translated.titleLite} + + {translated.titleCard} + + {translated.titleButton} + + {translated.title1} + + {translated.title4} + + {translated.title12} + + {translated.title13} + + + + ) +} + +export default TabsDemo diff --git a/src/packages/verticaltabs/demo.tsx b/src/packages/verticaltabs/demo.tsx new file mode 100644 index 0000000000..55e9b4eba2 --- /dev/null +++ b/src/packages/verticaltabs/demo.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { useTranslate } from '../../sites/assets/locale' +import Demo1 from './demos/h5/demo1' +import Demo2 from './demos/h5/demo2' +import Demo3 from './demos/h5/demo3' +import Demo4 from './demos/h5/demo4' +import Demo5 from './demos/h5/demo5' +import Demo6 from './demos/h5/demo6' +import Demo7 from './demos/h5/demo7' +import Demo8 from './demos/h5/demo8' + +const VeriticalTabsDemo = () => { + const [translated] = useTranslate({ + 'zh-CN': { + basic: '基础用法', + title1: '基础用法-微笑曲线', + titleLite: '基础用法-简约模式', + titleCard: '基础用法-卡片模式', + titleButton: '基础用法-按钮模式', + titleDivider: '基础用法-分割线模式', + title4: '数量多,滚动操作', + title12: '嵌套布局', + title13: '嵌套布局2', + }, + 'en-US': { + basic: 'Basic Usage', + title1: 'Basic Usage - Smile Curve', + titleLite: 'Basic Usage - Simple Mode', + titleCard: 'Basic Usage - Card Mode', + titleButton: 'Basic Usage - Button Mode', + titleDivider: 'Basic Usage - Divider Mode', + title4: 'A Large Number Of Scrolling Operations', + title12: 'Tabs In Tabs', + title13: 'Tabs In Tabs 2', + }, + }) + + return ( + <> +
+

{translated.basic}

+ +

{translated.titleLite}

+ +

{translated.titleCard}

+ +

{translated.titleButton}

+ +

{translated.title1}

+ +

{translated.title4}

+ +

{translated.title12}

+ +

{translated.title13}

+ +
+ + ) +} + +export default VeriticalTabsDemo diff --git a/src/packages/verticaltabs/demos/h5/demo1.tsx b/src/packages/verticaltabs/demos/h5/demo1.tsx new file mode 100644 index 0000000000..b5f41259d4 --- /dev/null +++ b/src/packages/verticaltabs/demos/h5/demo1.tsx @@ -0,0 +1,25 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react' + +const Demo1 = () => { + const [tab5value, setTab5value] = useState('0') + const list5 = Array.from(new Array(3).keys()) + return ( + <> + { + setTab5value(value) + }} + > + {list5.map((item) => ( + + Tab {item + 1} + + ))} + + + ) +} +export default Demo1 diff --git a/src/packages/verticaltabs/demos/h5/demo2.tsx b/src/packages/verticaltabs/demos/h5/demo2.tsx new file mode 100644 index 0000000000..9eb0211746 --- /dev/null +++ b/src/packages/verticaltabs/demos/h5/demo2.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react' + +const Demo2 = () => { + const [tab1value, setTab1value] = useState('0') + return ( + <> + { + setTab1value(value) + }} + activeType="simple" + > + Tab 1 + Tab 2 + Tab 3 + + + ) +} +export default Demo2 diff --git a/src/packages/verticaltabs/demos/h5/demo3.tsx b/src/packages/verticaltabs/demos/h5/demo3.tsx new file mode 100644 index 0000000000..c9a3238ef1 --- /dev/null +++ b/src/packages/verticaltabs/demos/h5/demo3.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react' + +const Demo3 = () => { + const [tab1value, setTab1value] = useState('0') + return ( + <> + { + setTab1value(value) + }} + activeType="simple" + > + Tab 1 + Tab 2 + Tab 3 + + + ) +} +export default Demo3 diff --git a/src/packages/verticaltabs/demos/h5/demo4.tsx b/src/packages/verticaltabs/demos/h5/demo4.tsx new file mode 100644 index 0000000000..cc24ce5b09 --- /dev/null +++ b/src/packages/verticaltabs/demos/h5/demo4.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react' + +const Demo4 = () => { + const [tab1value, setTab1value] = useState('0') + return ( + <> + { + setTab1value(value) + }} + activeType="button" + > + Tab 1 + Tab 2 + Tab 3 + + + ) +} +export default Demo4 diff --git a/src/packages/verticaltabs/demos/h5/demo5.tsx b/src/packages/verticaltabs/demos/h5/demo5.tsx new file mode 100644 index 0000000000..70cb145f65 --- /dev/null +++ b/src/packages/verticaltabs/demos/h5/demo5.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react' + +const Demo5 = () => { + const [tab1value, setTab1value] = useState('0') + return ( + <> + { + setTab1value(value) + }} + activeType="smile" + > + Tab 1 + Tab 2 + Tab 3 + + + ) +} +export default Demo5 diff --git a/src/packages/verticaltabs/demos/h5/demo6.tsx b/src/packages/verticaltabs/demos/h5/demo6.tsx new file mode 100644 index 0000000000..b617bc63fe --- /dev/null +++ b/src/packages/verticaltabs/demos/h5/demo6.tsx @@ -0,0 +1,25 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react' + +const Demo6 = () => { + const [tab4value, setTab4value] = useState('0') + const list4 = Array.from(new Array(10).keys()) + return ( + <> + { + setTab4value(value) + }} + > + {list4.map((item) => ( + + Tab {item} + + ))} + + + ) +} +export default Demo6 diff --git a/src/packages/tabs/demos/h5/demo21.tsx b/src/packages/verticaltabs/demos/h5/demo7.tsx similarity index 86% rename from src/packages/tabs/demos/h5/demo21.tsx rename to src/packages/verticaltabs/demos/h5/demo7.tsx index df3f25c65e..638ce3e4dd 100644 --- a/src/packages/tabs/demos/h5/demo21.tsx +++ b/src/packages/verticaltabs/demos/h5/demo7.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react' -const Demo21 = () => { +const Demo8 = () => { const [tab8value, setTab8value] = useState('0') const [tab9value, setTab9value] = useState('0') return ( @@ -11,7 +11,6 @@ const Demo21 = () => { onChange={(value) => { setTab8value(value) }} - autoHeight > { onChange={(value) => { setTab9value(value) }} - direction="vertical" > Tab 1 Tab 2 @@ -32,4 +30,4 @@ const Demo21 = () => { ) } -export default Demo21 +export default Demo8 diff --git a/src/packages/tabs/demos/taro/demo21.tsx b/src/packages/verticaltabs/demos/h5/demo8.tsx similarity index 51% rename from src/packages/tabs/demos/taro/demo21.tsx rename to src/packages/verticaltabs/demos/h5/demo8.tsx index 5bf0a7a54b..e6d03c5c3b 100644 --- a/src/packages/tabs/demos/taro/demo21.tsx +++ b/src/packages/verticaltabs/demos/h5/demo8.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react-taro' +import { Tabs, VerticalTabs } from '@nutui/nutui-react' -const Demo21 = () => { - const [tab8value, setTab8value] = useState('0') - const [tab9value, setTab9value] = useState('0') +const Demo8 = () => { + const [tab8value, setTab8value] = useState('0') + const [tab9value, setTab9value] = useState('0') return ( <> { autoHeight > - { setTab9value(value) }} - direction="vertical" > - Tab 1 - Tab 2 - Tab 3 - + Tab 1 + Tab 2 + Tab 3 + Tab 2 Tab 3 @@ -32,4 +31,4 @@ const Demo21 = () => { ) } -export default Demo21 +export default Demo8 diff --git a/src/packages/verticaltabs/demos/taro/demo1.tsx b/src/packages/verticaltabs/demos/taro/demo1.tsx new file mode 100644 index 0000000000..06827d4346 --- /dev/null +++ b/src/packages/verticaltabs/demos/taro/demo1.tsx @@ -0,0 +1,25 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react-taro' + +const Demo1 = () => { + const [tab5value, setTab5value] = useState('0') + const list5 = Array.from(new Array(3).keys()) + return ( + <> + { + setTab5value(value) + }} + > + {list5.map((item) => ( + + Tab {item + 1} + + ))} + + + ) +} +export default Demo1 diff --git a/src/packages/verticaltabs/demos/taro/demo2.tsx b/src/packages/verticaltabs/demos/taro/demo2.tsx new file mode 100644 index 0000000000..b7816da9bb --- /dev/null +++ b/src/packages/verticaltabs/demos/taro/demo2.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react-taro' + +const Demo2 = () => { + const [tab1value, setTab1value] = useState('0') + return ( + <> + { + setTab1value(value) + }} + activeType="simple" + > + Tab 1 + Tab 2 + Tab 3 + + + ) +} +export default Demo2 diff --git a/src/packages/verticaltabs/demos/taro/demo3.tsx b/src/packages/verticaltabs/demos/taro/demo3.tsx new file mode 100644 index 0000000000..9194e0ea02 --- /dev/null +++ b/src/packages/verticaltabs/demos/taro/demo3.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react-taro' + +const Demo3 = () => { + const [tab1value, setTab1value] = useState('0') + return ( + <> + { + setTab1value(value) + }} + activeType="simple" + > + Tab 1 + Tab 2 + Tab 3 + + + ) +} +export default Demo3 diff --git a/src/packages/verticaltabs/demos/taro/demo4.tsx b/src/packages/verticaltabs/demos/taro/demo4.tsx new file mode 100644 index 0000000000..d0252440e1 --- /dev/null +++ b/src/packages/verticaltabs/demos/taro/demo4.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react-taro' + +const Demo4 = () => { + const [tab1value, setTab1value] = useState('0') + return ( + <> + { + setTab1value(value) + }} + activeType="button" + > + Tab 1 + Tab 2 + Tab 3 + + + ) +} +export default Demo4 diff --git a/src/packages/verticaltabs/demos/taro/demo5.tsx b/src/packages/verticaltabs/demos/taro/demo5.tsx new file mode 100644 index 0000000000..624284dc14 --- /dev/null +++ b/src/packages/verticaltabs/demos/taro/demo5.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react-taro' + +const Demo5 = () => { + const [tab1value, setTab1value] = useState('0') + return ( + <> + { + setTab1value(value) + }} + activeType="smile" + > + Tab 1 + Tab 2 + Tab 3 + + + ) +} +export default Demo5 diff --git a/src/packages/verticaltabs/demos/taro/demo6.tsx b/src/packages/verticaltabs/demos/taro/demo6.tsx new file mode 100644 index 0000000000..fecdaa6e55 --- /dev/null +++ b/src/packages/verticaltabs/demos/taro/demo6.tsx @@ -0,0 +1,25 @@ +import React, { useState } from 'react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react-taro' + +const Demo6 = () => { + const [tab4value, setTab4value] = useState('0') + const list4 = Array.from(new Array(10).keys()) + return ( + <> + { + setTab4value(value) + }} + > + {list4.map((item) => ( + + Tab {item} + + ))} + + + ) +} +export default Demo6 diff --git a/src/packages/tabs/demos/h5/demo20.tsx b/src/packages/verticaltabs/demos/taro/demo7.tsx similarity index 85% rename from src/packages/tabs/demos/h5/demo20.tsx rename to src/packages/verticaltabs/demos/taro/demo7.tsx index 2b2c7c458c..9d65eeffdd 100644 --- a/src/packages/tabs/demos/h5/demo20.tsx +++ b/src/packages/verticaltabs/demos/taro/demo7.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' -import { Tabs } from '@nutui/nutui-react' +import { VerticalTabs as Tabs } from '@nutui/nutui-react-taro' -const Demo20 = () => { +const Demo8 = () => { const [tab8value, setTab8value] = useState('0') const [tab9value, setTab9value] = useState('0') return ( @@ -11,7 +11,6 @@ const Demo20 = () => { onChange={(value) => { setTab8value(value) }} - direction="vertical" > { onChange={(value) => { setTab9value(value) }} - direction="horizontal" > Tab 1 Tab 2 @@ -32,4 +30,4 @@ const Demo20 = () => { ) } -export default Demo20 +export default Demo8 diff --git a/src/packages/verticaltabs/demos/taro/demo8.tsx b/src/packages/verticaltabs/demos/taro/demo8.tsx new file mode 100644 index 0000000000..638d8278c8 --- /dev/null +++ b/src/packages/verticaltabs/demos/taro/demo8.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react' +import { Tabs, VerticalTabs } from '@nutui/nutui-react-taro' + +const Demo8 = () => { + const [tab8value, setTab8value] = useState('0') + const [tab9value, setTab9value] = useState('0') + return ( + <> + { + setTab8value(value) + }} + autoHeight + > + + { + setTab9value(value) + }} + > + Tab 1 + Tab 2 + Tab 3 + + + Tab 2 + Tab 3 + + + ) +} +export default Demo8 diff --git a/src/packages/verticaltabs/doc.en-US.md b/src/packages/verticaltabs/doc.en-US.md new file mode 100644 index 0000000000..0844538293 --- /dev/null +++ b/src/packages/verticaltabs/doc.en-US.md @@ -0,0 +1,135 @@ +# VerticalTabs vertical tabs + +Commonly used for storage and display of large blocks of content in horizontal areas, supports embedded tag format and rendering loop data format + +## Import + +```tsx +import { VerticalTabs } from '@nutui/nutui-react' +``` + +## Sample code + +### Basic usage + +:::demo + + + +::: + +### Basic usage-simple mode + +:::demo + + + +::: + +### Basic usage-card mode + +:::demo + + + +::: + +### Basic usage - button mode + +:::demo + + + +::: + +### Basic usage - smile curve + +:::demo + + + +::: + +### Large number, scrolling operation + +:::demo + + + +::: + +### Nested layout + +:::demo + + + +::: + +### Nested layout 2 + +:::demo + + + +::: + +## VerticalTabs + +### Props + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| value | The value of the currently active tab panel | `number` \| `string` | `0` | +| defaultValue | Initialize the value of the active tab | `number` \| `string` | `0` | +| activeColor | Label selected color | `string` | `#1A1A1A` | +| activeType | Select the bottom display style Optional values `line`、`smile`、`simple`、`card`、`button`、`divider` | `line` \| `smile` \| `simple` \| `card` \| `button`\| `divider` | `line` | +| duration | Switch animation duration, unit ms 0 means no animation | `number` \| `string` | `300` | +| title | custom navigation area | `() => JSX.Element[]` | `-` | +| align | title alignment | `left` \| `right` | `-` | +| autoHeight | Auto height. When set to true, nut-tabs and nut-tabs\_\_content will change with the height of the current nut-tabpane. | `boolean` | `false` | +| tabStyle | tab bar style | `CSSProperties` | `{}` | +| onClick | Triggered when the label is clicked | `(index: string \| number) => void` | `-` | +| onChange | Triggered when the currently active tab changes | `(index: string \| number) => void` | `-` | + +## VerticalTabs.Tabpane + +### Props + +| Property | Description | type | Default | +| --- | --- | --- | --- | +| title | title | `string` | `-` | +| value | tag Key , matching identifier, default is index value | `string` \| `number` | `-` | +| disabled | Whether to disable the label | `boolean` | `false` | + +## Theming + +### CSS Variables + +The component provides the following CSS variables, which can be used to customize styles. Please refer to [ConfigProvider component](#/en-US/component/configprovider). + +| 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-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-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-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-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%)` | +| \--nutui-tabs-vertical-tab-line-width | Vertical title line width | `3px` | +| \--nutui-tabs-vertical-tab-line-height | The height of the vertical title line | `12px` | +| \--nutui-tabs-tabpane-padding | Padding of the Tabpane content | `24px 20px` | +| \--nutui-tabs-tabpane-backgroundColor | BackgroundColor of the Tabpane content | `#fff` | diff --git a/src/packages/verticaltabs/doc.md b/src/packages/verticaltabs/doc.md new file mode 100644 index 0000000000..ffd3fe9995 --- /dev/null +++ b/src/packages/verticaltabs/doc.md @@ -0,0 +1,135 @@ +# VerticalTabs 垂直选项卡 + +常用于平级区域大块内容的的收纳和展现,支持内嵌标签形式和渲染循环数据形式 + +## 引入 + +```tsx +import { VerticalTabs } from '@nutui/nutui-react' +``` + +## 示例代码 + +### 基础用法 + +:::demo + + + +::: + +### 基础用法-简约模式 + +:::demo + + + +::: + +### 基础用法-卡片模式 + +:::demo + + + +::: + +### 基础用法-按钮模式 + +:::demo + + + +::: + +### 基础用法-微笑曲线 + +:::demo + + + +::: + +### 数量多,滚动操作 + +:::demo + + + +::: + +### 嵌套布局 + +:::demo + + + +::: + +### 嵌套布局2 + +:::demo + + + +::: + +## VerticalTabs + +### Props + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 当前激活 tab 面板的值 | `number` \| `string` | `0` | +| defaultValue | 初始化激活 tab 的值 | `number` \| `string` | `0` | +| activeColor | 标签选中色 | `string` | `#1A1A1A` | +| activeType | 选中底部展示样式 可选值 `line`、`smile`、`simple`、`card`、`button`、`divider` | `line` \| `smile` \| `simple` \| `card` \| `button`\| `divider` | `line` | +| duration | 切换动画时长,单位 ms 0 代表无动画 | `number` \| `string` | `300` | +| title | 自定义导航区域 | `() => JSX.Element[]` | `-` | +| align | 标题对齐方式 | `left` \| `right` | `-` | +| autoHeight | 自动高度。设置为 true 时,nut-tabs 和 nut-tabs\_\_content 会随着当前 nut-tabpane 的高度而发生变化。 | `boolean` | `false` | +| tabStyle | 标签栏样式 | `CSSProperties` | `{}` | +| onClick | 点击标签时触发 | `(index: string \| number) => void` | `-` | +| onChange | 当前激活的标签改变时触发 | `(index: string \| number) => void` | `-` | + +## VerticalTabs.Tabpane + +### Props + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| title | 标题 | `string` | `-` | +| value | 标签 Key , 匹配的标识符, 默认为索引值 | `string` \| `number` | `-` | +| disabled | 是否禁用标签 | `boolean` | `false` | + +## 主题定制 + +### 样式变量 + +组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/component/configprovider)。 + +| 名称 | 说明 | 默认值 | +| --- | --- | --- | +| \--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-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-background-color | 水平方向激活选项卡标题的背景色 | `$color-background-overlay` | +| \--nutui-tabs-tab-line-width | 水平方向激活选项卡线条的宽度 | `12px` | +| \--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-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%)` | +| \--nutui-tabs-vertical-tab-line-width | 垂直方向标题线条的宽度 | `3px` | +| \--nutui-tabs-vertical-tab-line-height | 垂直方向标题线条的高度 | `12px` | +| \--nutui-tabs-tabpane-padding | Tabpane 的内边距 | `24px 20px` | +| \--nutui-tabs-tabpane-backgroundColor | Tabpane 的背景色 | `#fff` | diff --git a/src/packages/verticaltabs/doc.taro.md b/src/packages/verticaltabs/doc.taro.md new file mode 100644 index 0000000000..13a721ca9d --- /dev/null +++ b/src/packages/verticaltabs/doc.taro.md @@ -0,0 +1,135 @@ +# VerticalTabs 垂直选项卡 + +常用于平级区域大块内容的的收纳和展现,支持内嵌标签形式和渲染循环数据形式 + +## 引入 + +```tsx +import { VerticalTabs } from '@nutui/nutui-react-taro' +``` + +## 示例代码 + +### 基础用法 + +:::demo + + + +::: + +### 基础用法-简约模式 + +:::demo + + + +::: + +### 基础用法-卡片模式 + +:::demo + + + +::: + +### 基础用法-按钮模式 + +:::demo + + + +::: + +### 基础用法-微笑曲线 + +:::demo + + + +::: + +### 数量多,滚动操作 + +:::demo + + + +::: + +### 嵌套布局 + +:::demo + + + +::: + +### 嵌套布局2 + +:::demo + + + +::: + +## Tabs + +### Props + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| value | 当前激活 tab 面板的值 | `number` \| `string` | `0` | +| defaultValue | 初始化激活 tab 的值 | `number` \| `string` | `0` | +| activeColor | 标签选中色 | `string` | `#1A1A1A` | +| activeType | 选中底部展示样式 可选值 `line`、`smile`、`simple`、`card`、`button`、`divider` | `line` \| `smile` \| `simple` \| `card` \| `button`\| `divider` | `line` | +| duration | 切换动画时长,单位 ms 0 代表无动画 | `number` \| `string` | `300` | +| title | 自定义导航区域 | `() => JSX.Element[]` | `-` | +| align | 标题对齐方式 | `left` \| `right` | `-` | +| autoHeight | 自动高度。设置为 true 时,nut-tabs 和 nut-tabs\_\_content 会随着当前 nut-tabpane 的高度而发生变化。 | `boolean` | `false` | +| tabStyle | 标签栏样式 | `CSSProperties` | `{}` | +| onClick | 点击标签时触发 | `(index: string\| number) => void` | `-` | +| onChange | 当前激活的标签改变时触发 | `(index: string \| number) => void` | `-` | + +## Tabs.Tabpane + +### Props + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| title | 标题 | `string` | `-` | +| value | 标签 Key , 匹配的标识符, 默认为索引值 | `string` \| `number` | `-` | +| disabled | 是否禁用标签 | `boolean` | `false` | + +## 主题定制 + +### 样式变量 + +组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/component/configprovider)。 + +| 名称 | 说明 | 默认值 | +| --- | --- | --- | +| \--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-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-background-color | 水平方向激活选项卡标题的背景色 | `$color-background-overlay` | +| \--nutui-tabs-tab-line-width | 水平方向激活选项卡线条的宽度 | `12px` | +| \--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-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%)` | +| \--nutui-tabs-vertical-tab-line-width | 垂直方向标题线条的宽度 | `3px` | +| \--nutui-tabs-vertical-tab-line-height | 垂直方向标题线条的高度 | `12px` | +| \--nutui-tabs-tabpane-padding | Tabpane 的内边距 | `24px 20px` | +| \--nutui-tabs-tabpane-backgroundColor | Tabpane 的背景色 | `#fff` | diff --git a/src/packages/verticaltabs/doc.zh-TW.md b/src/packages/verticaltabs/doc.zh-TW.md new file mode 100644 index 0000000000..f3d4361c4c --- /dev/null +++ b/src/packages/verticaltabs/doc.zh-TW.md @@ -0,0 +1,135 @@ +# VerticalTabs 垂直選項卡 + +常用於平級區域大塊內容的的收納和展現,支持內嵌標簽形式和渲染循環數據形式 + +## 引入 + +```tsx +import { VerticalTabs } from '@nutui/nutui-react' +``` + +## 示例代碼 + +### 基礎用法 + +:::demo + + + +::: + +### 基礎用法-簡約模式 + +:::demo + + + +::: + +### 基礎用法-卡片模式 + +:::demo + + + +::: + +### 基礎用法-按鈕模式 + +:::demo + + + +::: + +### 基礎用法-微笑曲線 + +:::demo + + + +::: + +### 數量多,滾動操作 + +:::demo + + + +::: + +### 嵌套布局 + +:::demo + + + +::: + +### 嵌套布局2 + +:::demo + + + +::: + +## VerticalTabs + +### Props + +| 屬性 | 說明 | 類型 | 默認值 | +| --- | --- | --- | --- | +| value | 當前激活 tab 面板的值 | `number` \| `string` | `0` | +| defaultValue | 初始化激活 tab 的值 | `number` \| `string` | `0` | +| activeColor | 標簽選中色 | `string` | `#1A1A1A` | +| activeType | 選中底部展示樣式 可選值 `line`、`smile`、`simple`、`card`、`button`、`divider` | `line` \| `smile` \| `simple` \| `card` \| `button`\| `divider` | `line` | +| duration | 切換動畫時長,單位 ms 0 代表無動畫 | `number` \| `string` | `300` | +| title | 自定義導航區域 | `() => JSX.Element[]` | `-` | +| align | 標題對齊方式 | `left` \| `right` | `-` | +| autoHeight | 自動高度。設置為 true 時,nut-tabs 和 nut-tabs\_\_content 會隨著當前 nut-tabpane 的高度而發生變化。 | `boolean` | `false` | +| tabStyle | 標簽欄樣式 | `CSSProperties` | `{}` | +| onClick | 點擊標簽時觸發 | `(index: string \| number) => void` | `-` | +| onChange | 當前激活的標簽改變時觸發 | `(index: string \| number) => void` | `-` | + +## VerticalTabs.Tabpane + +### Props + +| 屬性 | 說明 | 類型 | 默認值 | +| --- | --- | --- | --- | +| title | 標題 | `string` | `-` | +| value | 標簽 Key , 匹配的標識符, 默認為索引值 | `string` \| `number` | `-` | +| disabled | 是否禁用標簽 | `boolean` | `false` | + +## 主題定製 + +### 樣式變量 + +組件提供了下列 CSS 變量,可用於自定義樣式,使用方法請參考 [ConfigProvider 組件](#/zh-CN/component/configprovider)。 + +| 名稱 | 說明 | 默認值 | +| --- | --- | --- | +| \--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-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-background-color | 水平方向激活選項卡標題的背景色 | `$color-background-overlay` | +| \--nutui-tabs-tab-line-width | 水平方向激活選項卡線條的寬度 | `12px` | +| \--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-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%)` | +| \--nutui-tabs-vertical-tab-line-width | 垂直方向標題線條的寬度 | `3px` | +| \--nutui-tabs-vertical-tab-line-height | 垂直方向標題線條的高度 | `12px` | +| \--nutui-tabs-tabpane-padding | Tabpane 的內邊距 | `24px 20px` | +| \--nutui-tabs-tabpane-backgroundColor | Tabpane 的背景色 | `#fff` | diff --git a/src/packages/verticaltabs/index.taro.ts b/src/packages/verticaltabs/index.taro.ts new file mode 100644 index 0000000000..b43ea97f7a --- /dev/null +++ b/src/packages/verticaltabs/index.taro.ts @@ -0,0 +1,4 @@ +import { VerticalTabs } from './verticaltabs.taro' + +export type { TabsProps, TabsTitle } from '../tabs' +export default VerticalTabs diff --git a/src/packages/verticaltabs/index.ts b/src/packages/verticaltabs/index.ts new file mode 100644 index 0000000000..04de341e0d --- /dev/null +++ b/src/packages/verticaltabs/index.ts @@ -0,0 +1,4 @@ +import { VerticalTabs } from './verticaltabs' + +export type { TabsProps, TabsTitle } from '../tabs' +export default VerticalTabs diff --git a/src/packages/verticaltabs/verticaltabs.harmony.css b/src/packages/verticaltabs/verticaltabs.harmony.css new file mode 100644 index 0000000000..a436db6078 --- /dev/null +++ b/src/packages/verticaltabs/verticaltabs.harmony.css @@ -0,0 +1,360 @@ +.nut-tabs { + display: flex; +} + +.nut-tabs-titles { + display: flex; + box-sizing: border-box; + height: 44px; + padding: 0 16px; + user-select: none; + overflow: hidden; + background: #F7F8FC; +} +.nut-tabs-titles::-webkit-scrollbar { + display: none; + width: 0; + background: transparent; +} +.nut-tabs-titles .nut-tabs-list { + width: 100%; + height: auto; + display: flex; + flex-shrink: 0; +} +.nut-tabs-titles-left { + justify-content: flex-start; +} +.nut-tabs-titles-left .nut-tabs-titles-item { + padding: 0 10px; +} +.nut-tabs-titles-right { + justify-content: flex-end; +} +.nut-tabs-titles-right .nut-tabs-titles-item { + padding: 0 10px; +} +.nut-tabs-titles-scrollable { + overflow-x: auto; + overflow-y: hidden; +} +.nut-tabs-titles-item { + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex: 1 0 auto; + margin: 0 12px; + height: 44px; + line-height: 44px; + min-width: 50px; + font-size: 14px; + color: #1A1A1A; + text-overflow: ellipsis; + white-space: nowrap; +} +.nut-tabs-titles-item-left, .nut-tabs-titles-item-right { + flex: none; +} +.nut-tabs-titles-item-text { + text-align: center; +} +.nut-tabs-titles-item-smile { + position: absolute; + transition: width 0.3s ease; + width: 0; + height: 0; + content: " "; + bottom: 15%; + left: 50%; + transform: translate(-50%, 0); + border-radius: 2px; + opacity: 1; + overflow: hidden; +} +.nut-tabs-titles-item-line { + position: absolute; + transition: width 0.3s ease; + width: 0; + height: 0; + content: " "; + bottom: 15%; + left: 50%; + transform: translate(-50%, 0); + border-radius: 2px; + opacity: 1; + overflow: hidden; +} +.nut-tabs-titles-item-smile { + bottom: -10%; +} +.nut-tabs-titles-item-smile .nut-icon { + position: absolute; + font-size: 20px; + width: 100%; + height: 100%; + color: #FF0F23; +} +.nut-tabs-titles-item-active { + color: #FF0F23; + font-weight: 500; +} +.nut-tabs-titles-item-active .nut-tabs-titles-item-line { + overflow: unset; + content: " "; + width: 12px; + height: 2px; + background: #FF0F23; +} +.nut-tabs-titles-item-active .nut-tabs-titles-item-smile { + overflow: unset; + width: 40px; + height: 20px; +} +.nut-tabs-titles-item-disabled { + color: #C2C4CC; +} +.nut-tabs-titles-item:first-child { + margin-left: 0; +} +.nut-tabs-titles-item:last-child { + margin-right: 0; +} +.nut-tabs-titles-simple .nut-tabs-titles-item-active { + color: #1A1A1A; + font-size: 16px; +} +.nut-tabs-titles-card { + padding: 0; + background-color: undefined; +} +.nut-tabs-titles-card .nut-tabs-titles-item { + margin: 0; +} +.nut-tabs-titles-card .nut-tabs-titles-item-active { + font-weight: 500; + background-color: #ffffff; + border-radius: undefined; +} +.nut-tabs-titles-button .nut-tabs-titles-item-active { + height: 28px; + margin-top: 8px; + margin-bottom: 8px; + background: undefined; + color: #505259; + border-radius: 50px; + font-weight: 500; + background-color: #FFEBF1; + color: #FF0F23; + border: 1px solid #FF0F23; +} +.nut-tabs-titles-divider { + padding: 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); +} +.nut-tabs-titles-divider .nut-tabs-titles-item { + margin: 0; + position: relative; +} +.nut-tabs-titles-divider .nut-tabs-titles-item::after { + content: ""; + position: absolute; + right: 0; + top: 50%; + height: 50%; + width: 1px; + background: rgba(0, 0, 0, 0.06); + transform: translateY(-50%); +} +.nut-tabs-titles-divider .nut-tabs-titles-item:last-child::after { + display: none; +} + +[dir=rtl] .nut-tabs-titles-item-smile, [dir=rtl] .nut-tabs-titles-item-line, +.nut-rtl .nut-tabs-titles-item-smile, +.nut-rtl .nut-tabs-titles-item-line { + left: auto; + right: 50%; + transform: translate(50%, 0); +} +[dir=rtl] .nut-tabs-titles-item:first-child, +.nut-rtl .nut-tabs-titles-item:first-child { + margin-left: 0; + margin-right: 0; +} +[dir=rtl] .nut-tabs-titles-divider .nut-tabs-titles-item::after, +.nut-rtl .nut-tabs-titles-divider .nut-tabs-titles-item::after { + right: auto; + left: 0; +} + +.nut-tabs-horizontal { + flex-direction: column; + position: relative; +} + +.nut-tabs-vertical { + flex-direction: row; + width: 100%; +} +.nut-tabs-vertical .nut-tabs-ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} +.nut-tabs-vertical .nut-tabs-titles { + box-sizing: border-box; + flex-direction: column; + height: 100%; + padding: 0; + width: 100px; + flex-shrink: 0; +} +.nut-tabs-vertical .nut-tabs-titles .nut-tabs-list { + width: 100%; + display: flex; + flex-direction: column; + flex-shrink: 0; +} +.nut-tabs-vertical .nut-tabs-titles-line .nut-tabs-titles-item { + padding-left: 14px; +} +.nut-tabs-vertical .nut-tabs-titles-scrollable { + overflow-x: hidden; + overflow-y: auto; +} +.nut-tabs-vertical .nut-tabs-titles-item { + height: 40px; + margin: 0; + flex: none; +} +.nut-tabs-vertical .nut-tabs-titles-item-smile { + width: 0; + height: 0; + overflow: hidden; + transition: width 0.3s ease; +} +.nut-tabs-vertical .nut-tabs-titles-item-line { + width: 0; + height: 0; + transform: translate(0, -50%); + transition: height 0.3s ease; +} +.nut-tabs-vertical .nut-tabs-titles-item-line-vertical { + top: 50%; +} +.nut-tabs-vertical .nut-tabs-titles-item-active { + background-color: #ffffff; +} +.nut-tabs-vertical .nut-tabs-titles-item-active .nut-tabs-titles-item-line { + left: 10px; + width: 3px; + height: 12px; + background: linear-gradient(180deg, #FF0F23 0%, #FFEBF1 100%); +} +.nut-tabs-vertical .nut-tabs-titles-item-active .nut-tabs-titles-item-smile { + right: -12px; + bottom: -2%; + left: auto; + width: 40px; + height: 20px; + transform: rotate(320deg); +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles { + flex-direction: row; + overflow-x: auto; + overflow-y: hidden; + height: 44px; +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles .nut-tabs-list { + width: 100%; + display: flex; + flex-direction: row; + flex-shrink: 0; +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-content { + flex-direction: row; +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles-item-active { + background-color: initial; +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles-item-active .nut-tabs-titles-item-line { + left: 50%; + transform: translate(-50%, 0); +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles-item-active .nut-tabs-titles-item-smile { + left: 50%; + right: auto; + bottom: -3px; + transform: translate(-50%, 0) rotate(0deg); +} +.nut-tabs-vertical .nut-tabs-content { + flex-direction: column; + height: 100%; +} +.nut-tabs-vertical .nut-tabs-content-wrap { + flex: 1; +} +.nut-tabs-vertical .nut-tabs-content .nut-tabpane { + height: 100%; +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles { + display: flex; + flex-direction: row; + padding: 0 !important; + width: 100%; +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles .nut-tabs-titles-item { + display: flex; + align-items: center; + justify-content: center; + flex: 1 0 auto; +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles .nut-tabs-titles-item-active { + color: #FF0F23; + font-weight: 500; + font-size: 16px; +} +.nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles .nut-tabs-titles-item-active .nut-tabs-titles-item-line { + content: " "; + width: 12px; + height: 2px; + background: #FF0F23; +} + +[dir=rtl] .nut-tabs-vertical .nut-tabs-titles-line .nut-tabs-titles-item, +.nut-rtl .nut-tabs-vertical .nut-tabs-titles-line .nut-tabs-titles-item { + padding-left: 0; + padding-right: 14px; +} +[dir=rtl] .nut-tabs-vertical .nut-tabs-titles-item-active .nut-tabs-titles-item-line, +.nut-rtl .nut-tabs-vertical .nut-tabs-titles-item-active .nut-tabs-titles-item-line { + left: auto; + right: 10px; +} +[dir=rtl] .nut-tabs-vertical .nut-tabs-titles-item-active .nut-tabs-titles-item-smile, +.nut-rtl .nut-tabs-vertical .nut-tabs-titles-item-active .nut-tabs-titles-item-smile { + left: -12px; + right: auto; + transform: rotate(-320deg); +} +[dir=rtl] .nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles-item-active .nut-tabs-titles-item-line, +.nut-rtl .nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles-item-active .nut-tabs-titles-item-line { + left: auto; + right: 50%; + transform: translate(50%, 0); +} +[dir=rtl] .nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles-item-active .nut-tabs-titles-item-smile, +.nut-rtl .nut-tabs-vertical .nut-tabs-horizontal .nut-tabs-titles-item-active .nut-tabs-titles-item-smile { + right: 50%; + left: auto; + transform: translate(50%, 0) rotate(0deg); +} + +.nut-tabs-content { + display: flex; + box-sizing: border-box; +} +.nut-tabs-content-wrap { + overflow: hidden; +} \ No newline at end of file diff --git a/src/packages/verticaltabs/verticaltabs.scss b/src/packages/verticaltabs/verticaltabs.scss new file mode 100644 index 0000000000..244d93fae7 --- /dev/null +++ b/src/packages/verticaltabs/verticaltabs.scss @@ -0,0 +1,345 @@ +@import '../../styles/mixins/index'; +@import '../tabpane/tabpane.scss'; + +.nut-tabs { + display: flex; +} + +.nut-tabs-titles { + display: flex; + box-sizing: border-box; + height: $tabs-titles-height; + user-select: none; + overflow: hidden; + background: $tabs-titles-background-color; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + width: 0; + background: transparent; + } + + .nut-tabs-list { + width: 100%; + height: auto; + display: flex; + flex-shrink: 0; + } + + &-left { + justify-content: flex-start; + .nut-tabs-titles-item { + padding: 0 22px; + } + } + + &-right { + justify-content: flex-end; + .nut-tabs-titles-item { + padding: 0 22px; + } + } + + &-scrollable { + overflow-x: auto; + overflow-y: hidden; + } + + &-item { + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex: 1 0 auto; + padding: 0 $tabs-titles-gap; + height: $tabs-titles-height; + line-height: $tabs-titles-height; + min-width: $tabs-titles-item-min-width; + font-size: $tabs-titles-font-size; + color: $tabs-titles-item-color; + text-overflow: ellipsis; + white-space: nowrap; + + &-left, + &-right { + flex: none; + } + + &-text { + text-align: center; + } + + &-smile, + &-line { + position: absolute; + transition: width 0.3s ease; + width: 0; + height: 0; + content: ' '; + bottom: $tabs-tab-line-bottom; + left: 50%; + transform: translate(-50%, 0); + border-radius: $tabs-tab-line-border-radius; + opacity: $tabs-tab-line-opacity; + overflow: hidden; + } + + &-smile { + bottom: $tabs-titles-item-smile-bottom; + + .nut-icon { + position: absolute; + font-size: 20px; + width: 100%; + height: 100%; + color: $color-primary; + } + } + + &-active { + color: $tabs-titles-item-active-color; + font-weight: $tabs-titles-item-active-font-weight; + + .nut-tabs-titles-item-line { + overflow: unset; + content: ' '; + width: $tabs-tab-line-width; + height: $tabs-tab-line-height; + background: $tabs-tab-line-color; + } + + .nut-tabs-titles-item-smile { + overflow: unset; + width: 40px; + height: 20px; + } + } + + &-disabled { + color: $color-text-disabled; + } + } + + &-simple { + .nut-tabs-titles-item-active { + color: $color-title; + font-size: $tabs-titles-item-active-font-size; + } + } + + &-card { + padding: 0; + background-color: $color-default-light; + + .nut-tabs-titles-item { + padding: 0; + + &-active { + font-weight: $font-weight-bold; + background-color: $white; + border-radius: $radius-s $radius-s 0 0; + } + } + } + + &-button { + .nut-tabs-titles-item { + padding: 0 10px; + .nut-tabs-titles-item-text { + flex: 1; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 8px; + } + } + .nut-tabs-titles-item-active { + .nut-tabs-titles-item-text { + background: $color-default-light; + border-radius: $tabs-tab-button-border-radius; + font-weight: $font-weight-bold; + background-color: $tabs-tab-button-active-background-color; + border: $tabs-tab-button-active-border; + } + } + } + + &-divider { + padding: 0; + border-bottom: 1px solid $color-border; + + .nut-tabs-titles-item { + padding: 0; + position: relative; + + &::after { + content: ''; + position: absolute; + right: 0; + top: 50%; + height: 50%; + width: 1px; + background: $color-border; + transform: translateY(-50%); + } + + &:last-child { + &::after { + display: none; + } + } + } + } +} + +[dir='rtl'] .nut-tabs-titles, +.nut-rtl .nut-tabs-titles { + &-item { + &-smile, + &-line { + left: auto; + right: 50%; + transform: translate(50%, 0); + } + } + &-divider { + .nut-tabs-titles-item { + &::after { + right: auto; + left: 0; + } + } + } +} + +.nut-tabs-vertical { + flex-direction: row; + width: 100%; + + .nut-tabs-ellipsis { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .nut-tabs-titles { + box-sizing: border-box; + flex-direction: column; + height: 100%; + padding: 0; + width: $tabs-vertical-titles-width; + flex-shrink: 0; + + .nut-tabs-list { + width: 100%; + display: flex; + flex-direction: column; + flex-shrink: 0; + } + + &-line { + .nut-tabs-titles-item { + padding-left: 14px; + } + } + } + + .nut-tabs-titles-scrollable { + overflow-x: hidden; + overflow-y: auto; + } + + .nut-tabs-titles-item { + height: $tabs-vertical-titles-item-height; + margin: 0; + flex: none; + + &-smile { + width: 0; + height: 0; + overflow: hidden; + transition: width 0.3s ease; + } + + &-line { + width: 0; + height: 0; + transform: translate(0, -50%); + transition: height 0.3s ease; + + &-vertical { + top: 50%; + } + } + + &-active { + background-color: $tabs-titles-item-active-background-color; + .nut-tabs-titles-item-line { + left: 10px; + width: $tabs-vertical-tab-line-width; + height: $tabs-vertical-tab-line-height; + background: $tabs-vertical-tab-line-color; + } + + .nut-tabs-titles-item-smile { + right: -12px; + bottom: -2%; + left: auto; + width: 40px; + height: 20px; + transform: rotate(320deg); + } + } + } + + .nut-tabs-content { + flex-direction: column; + height: 100%; + + &-wrap { + flex: 1; + } + + .nut-tabpane { + height: 100%; + } + } +} + +[dir='rtl'] .nut-tabs-vertical, +.nut-rtl .nut-tabs-vertical { + .nut-tabs-titles { + &-line { + .nut-tabs-titles-item { + padding-left: 0; + padding-right: 14px; + } + } + } + + .nut-tabs-titles-item { + &-active { + .nut-tabs-titles-item-line { + left: auto; + right: 10px; + } + + .nut-tabs-titles-item-smile { + left: -12px; + right: auto; + transform: rotate(-320deg); + } + } + } +} + +.nut-tabs-content { + display: flex; + box-sizing: border-box; + + &-wrap { + overflow: hidden; + } +} diff --git a/src/packages/verticaltabs/verticaltabs.taro.tsx b/src/packages/verticaltabs/verticaltabs.taro.tsx new file mode 100644 index 0000000000..76a0720320 --- /dev/null +++ b/src/packages/verticaltabs/verticaltabs.taro.tsx @@ -0,0 +1,292 @@ +import React, { FunctionComponent, useEffect, useRef, useState } from 'react' +import { ScrollView, View } from '@tarojs/components' +import classNames from 'classnames' +import { JoySmile } from '@nutui/icons-react-taro' +import Taro, { nextTick, createSelectorQuery } from '@tarojs/taro' +import { ComponentDefaults } from '@/utils/typings' +import TabPane from '@/packages/tabpane/index.taro' +import { usePropsValue } from '@/utils/use-props-value' +import { useForceUpdate } from '@/utils/use-force-update' +import raf from '@/utils/raf' +import useUuid from '@/utils/use-uuid' +import { useRtl } from '../configprovider/configprovider.taro' +import { TabsTitle, TabsProps } from '../tabs' + +const defaultProps = { + ...ComponentDefaults, + tabStyle: {}, + activeColor: '', + activeType: 'line', + duration: 300, + autoHeight: false, +} as TabsProps + +const classPrefix = 'nut-tabs' +export const VerticalTabs: FunctionComponent> & { + TabPane: typeof TabPane +} = (props) => { + const rtl = useRtl() + const { + activeColor, + tabStyle, + activeType, + duration, + align, + title, + name, + children, + onClick, + onChange, + className, + autoHeight, + value: outerValue, + defaultValue: outerDefaultValue, + ...rest + } = { + ...defaultProps, + ...props, + } + const uuid = useUuid() + const [value, setValue] = usePropsValue({ + value: outerValue, + defaultValue: outerDefaultValue, + finalValue: 0, + onChange, + }) + + const titleItemsRef = useRef([]) + const navRef = useRef(null) + + const getTitles = () => { + const titles: TabsTitle[] = [] + React.Children.forEach(children, (child: any, idx) => { + if (React.isValidElement(child)) { + const props: any = child?.props + if (props?.title || props?.value) { + titles.push({ + title: props.title, + value: props.value || idx, + disabled: props.disabled, + }) + } + } + }) + return titles + } + + const titles = useRef(getTitles()) + const forceUpdate = useForceUpdate() + useEffect(() => { + titles.current = getTitles() + let current: string | number = '' + titles.current.forEach((title) => { + if (title.value === value) { + current = value + } + }) + if (current !== '' && current !== value) { + setValue(current) + } else { + forceUpdate() + } + }, [children]) + const classes = classNames(classPrefix, `${classPrefix}-vertical`, className) + const classesTitle = classNames(`${classPrefix}-titles`, { + [`${classPrefix}-titles-${activeType}`]: activeType, + [`${classPrefix}-titles-scrollable`]: true, + [`${classPrefix}-titles-${align}`]: align, + }) + + const tabsActiveStyle = { + color: activeType === 'smile' ? activeColor : '', + background: activeType === 'line' ? activeColor : '', + } + const getRect = (selector: string) => { + return new Promise((resolve) => { + createSelectorQuery() + .select(selector) + .boundingClientRect() + .exec((rect = []) => { + resolve(rect[0]) + }) + }) + } + const getAllRect = (selector: string) => { + return new Promise((resolve) => { + createSelectorQuery() + .selectAll(selector) + .boundingClientRect() + .exec((rect = []) => { + resolve(rect[0]) + }) + }) + } + type RectItem = { + bottom: number + dataset: { sid: string } + height: number + id: string + left: number + right: number + top: number + width: number + } + const scrollWithAnimation = useRef(false) + const navRectRef = useRef() + const titleRectRef = useRef([]) + const [scrollTop, setScrollTop] = useState(0) + const scrollDirection = (to: number) => { + let count = 0 + const frames = 1 + function animate() { + setScrollTop(to) + if (++count < frames) { + raf(animate) + } + } + animate() + } + const scrollIntoView = (index: number) => { + raf(() => { + Promise.all([ + getRect(`#nut-tabs-titles-${name || uuid} .nut-tabs-list`), + getAllRect(`#nut-tabs-titles-${name || uuid} .nut-tabs-titles-item`), + ]).then(([navRect, titleRects]: any) => { + navRectRef.current = navRect + titleRectRef.current = titleRects + const titleRect: RectItem = titleRectRef.current[index] + if (!titleRect) return + const top = titleRects + .slice(0, index) + .reduce((prev: number, curr: RectItem) => prev + curr.height, 0) + const to = top - (navRectRef.current.height - titleRect.height) / 2 + nextTick(() => { + scrollWithAnimation.current = true + }) + scrollDirection(to) + }) + }) + } + + const getContentStyle = () => { + let index = titles.current.findIndex( + (t) => String(t.value) === String(value) + ) + index = index < 0 ? 0 : index + return { + transform: `translate3d( 0,-${index * 100}%, 0)`, + transitionDuration: `${duration}ms`, + } + } + + useEffect(() => { + let index = titles.current.findIndex( + (t) => String(t.value) === String(value) + ) + index = index < 0 ? 0 : index + scrollIntoView(index) + }, [value]) + + const tabChange = (item: TabsTitle, index: number) => { + onClick && onClick(item.value) + if (item.disabled) return + setValue(item.value) + } + + return ( + + + + {!!title && typeof title === 'function' + ? title() + : titles.current.map((item, index) => { + return ( + + titleItemsRef.current.push(ref) + } + id={`scrollIntoView${index}`} + onClick={(e) => { + tabChange(item, index) + }} + className={classNames(`${classPrefix}-titles-item`, { + [`nut-tabs-titles-item-active`]: + !item.disabled && String(item.value) === String(value), + [`nut-tabs-titles-item-disabled`]: item.disabled, + [`nut-tabs-titles-item-${align}`]: align, + })} + key={item.value} + > + {activeType === 'line' && ( + + )} + {activeType === 'smile' && ( + + + + )} + + {item.title} + + + ) + })} + + + + + {React.Children.map(children, (child, idx) => { + if (!React.isValidElement(child)) { + return null + } + let childProps = { + ...child.props, + active: value === child.props.value, + } + if ( + String(value) !== String(child.props.value || idx) && + autoHeight + ) { + childProps = { + ...childProps, + autoHeightClassName: 'inactive', + } + } + return React.cloneElement(child, childProps) + })} + + + + ) +} + +VerticalTabs.displayName = 'NutVerticalTabs' +VerticalTabs.TabPane = TabPane diff --git a/src/packages/verticaltabs/verticaltabs.tsx b/src/packages/verticaltabs/verticaltabs.tsx new file mode 100644 index 0000000000..399812e8af --- /dev/null +++ b/src/packages/verticaltabs/verticaltabs.tsx @@ -0,0 +1,228 @@ +import React, { FunctionComponent, useEffect, useRef } from 'react' +import classNames from 'classnames' +import { JoySmile } from '@nutui/icons-react' +import { ComponentDefaults } from '@/utils/typings' +import TabPane from '@/packages/tabpane' +import raf from '@/utils/raf' +import { usePropsValue } from '@/utils/use-props-value' +import { useForceUpdate } from '@/utils/use-force-update' +import { useRtl } from '../configprovider' +import { TabsTitle, TabsProps } from '../tabs' + +const defaultProps = { + ...ComponentDefaults, + tabStyle: {}, + activeColor: '', + activeType: 'line', + duration: 300, + autoHeight: false, +} as TabsProps + +const classPrefix = 'nut-tabs' +export const VerticalTabs: FunctionComponent> & { + TabPane: typeof TabPane +} = (props) => { + const rtl = useRtl() + const { + activeColor, + tabStyle, + activeType, + duration, + align, + title, + children, + onClick, + onChange, + className, + autoHeight, + value: outerValue, + defaultValue: outerDefaultValue, + ...rest + } = { + ...defaultProps, + ...props, + } + + const [value, setValue] = usePropsValue({ + value: outerValue, + defaultValue: outerDefaultValue, + finalValue: 0, + onChange, + }) + const titleItemsRef = useRef([]) + const navRef = useRef(null) + const scrollDirection = (nav: any, to: number, duration: number) => { + let count = 0 + const from = nav.scrollTop + const frames = duration === 0 ? 1 : Math.round((duration * 1000) / 16) + + function animate() { + nav.scrollTop += (to - from) / frames + if (++count < frames) { + raf(animate) + } + } + animate() + } + const scrollIntoView = (index: number, immediate?: boolean) => { + const nav = navRef.current + const titleItem = titleItemsRef.current + const titlesLength = titles.current.length + const itemLength = titleItemsRef.current.length + if (!nav || !titleItem || !titleItem[itemLength - titlesLength + index]) { + return + } + const title = titleItem[itemLength - titlesLength + index] + const runTop = title.offsetTop - nav.offsetTop + 10 + const to = runTop - (nav.offsetHeight - title.offsetHeight) / 2 + scrollDirection(nav, to, immediate ? 0 : 0.3) + } + + const getTitles = () => { + const titles: TabsTitle[] = [] + React.Children.forEach(children, (child: any, idx) => { + if (React.isValidElement(child)) { + const props: any = child?.props + if (props?.title || props?.value) { + titles.push({ + title: props.title, + value: props.value || idx, + disabled: props.disabled, + }) + } + } + }) + return titles + } + const titles = useRef(getTitles()) + const forceUpdate = useForceUpdate() + useEffect(() => { + titles.current = getTitles() + let current: string | number = '' + titles.current.forEach((title) => { + // eslint-disable-next-line eqeqeq + if (title.value == value) { + current = value + } + }) + if (current !== '' && current !== value) { + setValue(current) + } else { + forceUpdate() + } + }, [children]) + + const classes = classNames(classPrefix, `${classPrefix}-vertical`, className) + const classesTitle = classNames(`${classPrefix}-titles`, { + [`${classPrefix}-titles-${activeType}`]: activeType, + [`${classPrefix}-titles-scrollable`]: true, + [`${classPrefix}-titles-${align}`]: align, + }) + + const tabsActiveStyle = { + color: activeType === 'smile' ? activeColor : '', + background: activeType === 'line' ? activeColor : '', + } + const getContentStyle = () => { + // eslint-disable-next-line eqeqeq + let index = titles.current.findIndex((t) => t.value == value) + index = index < 0 ? 0 : index + return { + transform: `translate3d( 0,-${index * 100}%, 0)`, + transitionDuration: `${duration}ms`, + } + } + useEffect(() => { + let index = titles.current.findIndex((t) => t.value === value) + index = index < 0 ? 0 : index + setTimeout(() => { + scrollIntoView(index) + }) + }, [value]) + + const tabChange = (item: TabsTitle) => { + onClick && onClick(item.value) + if (item.disabled) { + return + } + setValue(item.value) + } + return ( +
+
+ {!!title && typeof title === 'function' + ? title() + : titles.current.map((item) => { + return ( +
{ + tabChange(item) + }} + className={classNames(`${classPrefix}-titles-item`, { + [`nut-tabs-titles-item-active`]: + !item.disabled && String(item.value) === String(value), + [`nut-tabs-titles-item-disabled`]: item.disabled, + [`nut-tabs-titles-item-${align}`]: align, + })} + ref={(ref: HTMLDivElement) => titleItemsRef.current.push(ref)} + key={item.value} + > + {activeType === 'line' && ( +
+ )} + {activeType === 'smile' && ( +
+ +
+ )} +
+ {item.title} +
+
+ ) + })} +
+
+
+ {React.Children.map(children, (child, idx) => { + if (!React.isValidElement(child)) { + return null + } + + let childProps = { + ...child.props, + active: value === child.props.value, + } + + if ( + String(value) !== String(child.props.value || idx) && + autoHeight + ) { + childProps = { + ...childProps, + autoHeightClassName: 'inactive', + } + } + return React.cloneElement(child, childProps) + })} +
+
+
+ ) +} + +VerticalTabs.displayName = 'NutVerticalTabs' +VerticalTabs.TabPane = TabPane